之前折腾了了一下MINE模拟器,发现SDL全是在JNI层fopen
操作的,而安卓的SAF则是JAVA层通过DocumentFile和docUri来实现写入的。一种方法是通过去的File Descriptor然后传给JNI层,通过fdopen
实现写入[1]。于是成功在MINE模拟器添加外置sd卡写入功能,详见我在贴吧布的 mine模拟器外置SD卡写入修复版。完整版源码我已经封装好了发布到github上,理论上通用。
通过DocumentFile来实现写入,Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
发送请求docUri
,然后onActivityResult
来得到并储存docUri
,通过SharedPreference来实现共享。已经封装在静态类SafFile.java
中。
public static DocumentFile getBaseDocumentFile(final Context context, final SharedPreferences share) { if(context==null) { Log.e(LOGTAG, "SafFile.getBaseDocumentFile context is null!"); return null; } if(share==null){ Log.e(LOGTAG, "SafFile.getBaseDocumentFile share is null!"); return null; } DocumentFile base = null; Uri docUri = null; final String p = share.getString("docUri", null); if (p != null) docUri = Uri.parse(p); base = DocumentFile.fromTreeUri(context, docUri); return base; }
public static DocumentFile getTargetDirDocumentFile(final DocumentFile base, String path) { DocumentFile target = null; if (base == null) { Log.e(LOGTAG, "SafFile.getTargetDirDocumentFile base is null!"); return null; } if(path==null) path=""; path = path.replace("\\", "/"); final String paths[] = path.split("/"); int i; final int end = paths[paths.length - 1].length() > 0 ? paths.length - 1 : paths.length - 2; for (i = 0; i < end; i++) { // Log.i(LOGTAG, "getTar... path["+String.valueOf(i)+"], "+paths[i]); if (paths[i].equals(base.getName())) { if (i >= end - 1) { // Log.i(LOGTAG, "getTar... "+path+" end="+paths[paths.length-1]+" "+ paths[end]); return base; } i++; break; } } // Log.i(LOGTAG, "getTarget... "+base.getName()+" "+path); target = base.findFile(paths[i++]); // Log.i(LOGTAG, "target, "+ target.getName()); for (; i < end; i++) { if (target == null) break; // Log.i(LOGTAG, "getTar..., "+path+" "+ target.getName()); target = target.findFile(paths[i]); } return target; }
public static OutputStream getOutputStreamSaf(final Context context, final DocumentFile base, final String path, final boolean append) { if(context==null) { Log.e(LOGTAG, "SafFile.getOutputStreamSaf context is null!"); return null; } if(base==null){ Log.e(LOGTAG, "SafFile.getOutputStreamSaf base is null!"); return null; } OutputStream out = null; final String mode = append ? "wa" : "w"; // Log.i(LOGTAG, "getOut.. "+ path +" "+mode); final DocumentFile df2 = createFileSaf(base, path, append); if (df2 == null) { return null; } try { out = context.getContentResolver().openOutputStream(df2.getUri(), mode); } catch (final Exception e) { Log.e(LOGTAG, "SafFile.getOutputStreamSaf " + e.getClass().getName()); } return out; }
public static int getFdSaf(final Context context, final DocumentFile base, final String path, final String mode) { if(context==null) { Log.e(LOGTAG, "SafFile.getFdSaf context is null!"); return 0; } if(base==null){ Log.e(LOGTAG, "SafFile.getFdSaf base is null!"); return 0; } ParcelFileDescriptor pfd = null; boolean append = false; DocumentFile df2 = null; if (mode.indexOf('+') != -1 || mode.indexOf('a') != -1) append = true; if (mode.indexOf('w') == -1) append = true; df2 = createFileSaf(base, path, append); if (df2 == null) { Log.e(LOGTAG, "SafFile.getFdSaf, " + path + " error!"); return 0; } try { pfd = context.getContentResolver().openFileDescriptor(df2.getUri(), mode); } catch (final Exception e) { Log.e(LOGTAG, "SafFile.getFdSaf " + e.getClass().getName()); } if (pfd == null) return 0; return pfd.detachFd(); }
这里用到了xhook架构,原理上是运行的时候来替换目标动态库的.got
表到自己编译的函数地址,通过JNI来调用JAVA层我们写好通过SAF机制得到的文件描述符。
"com/yurisizuku/utils/SafFile"
void nativeInitSafJavaCallbacks(JNIEnv* env, jclass clazz) { LOGI("In nativeInitSafJavaCallbacks start!"); g_javaGetFD=(*env)->GetStaticMethodID(env, clazz, "getFD", "(Ljava/lang/String;Ljava/lang/String;I)I"); g_javaMkdir=(*env)->GetStaticMethodID(env, clazz, "mkdir", "(Ljava/lang/String;Ljava/lang/String;I)I"); g_javaRemove = (*env)->GetStaticMethodID(env, clazz, "remove", "(Ljava/lang/String;Ljava/lang/String;)I"); LOGI("In nativeInitSafJavaCallbacks finished!"); }
void nativeHookFile(JNIEnv* env, jclass clazz, jstring hooksoStr, jstring soPath) { char buf[100]; char *cstr_hooksoStr = jstr2cstr(env, hooksoStr); LOGI("nativeHookFile, %s \n", cstr_hooksoStr); char *cstr_soPath = jstr2cstr(env, soPath); if(cstr_soPath && strlen(cstr_soPath)) { if (!dlopen(cstr_soPath, RTLD_LAZY)) //dlopen in advance LOGE("dlopen(%s,%d) error!\n", cstr_soPath,RTLD_LAZY); else LOGI("dlopen(%s,%d) success !\n", cstr_soPath,RTLD_LAZY); } if (xhook_register(cstr_hooksoStr, "fopen", fopen_saf, NULL)) LOGE("xhook fopen register failed!"); else LOGI("xhook fopen register successed!"); if (xhook_register(cstr_hooksoStr, "mkdir", mkdir_saf, NULL)) LOGE("xhook mkdir register failed!\n"); else LOGI("xhook mkdir register successed!"); if (xhook_register(cstr_hooksoStr, "remove", remove_saf, NULL)) LOGE("xhook remove register failed!\n"); else LOGI("xhook remove register successed!"); xhook_refresh(0); free(cstr_hooksoStr); LOGI("nativeHookFile xhook finished!"); if(cstr_soPath) free(cstr_soPath); }
FILE *fopen_saf(const char *pathname, const char *mode) { FILE* fp=NULL; JNIEnv* env = NULL; (*g_vm)->AttachCurrentThread(g_vm, &env, NULL); if(!env) { LOGE("fopen_asf, env AttachCurrentThread failed!\n"); return fopen(pathname, mode); } int mode2=0; if(mode[0] == 'w') mode2=1; fp = fopen(pathname, mode); if(!(fp || mode2 == 0 || errno != EACCES)) { char buf[PATH_MAX_LEN]; getcwd(buf, PATH_MAX_LEN); //LOGI("before fopen(%s, %s), cwd=%s\n", pathname, mode, buf); jstring s_pathname = (*env)->NewStringUTF(env, pathname); jstring s_curdir = (*env)->NewStringUTF(env, buf); int fd = (*env)->CallStaticIntMethod(env, g_javaClass, g_javaGetFD, s_curdir, s_pathname, mode2 ); (*env)->DeleteLocalRef(env, s_curdir); (*env)->DeleteLocalRef(env, s_pathname); fp = fdopen(fd, mode); //LOGI("after fopen_saf(%s, %s),fp=%x, cwd=%s\n", pathname, mode, (unsigned int)fp,buf); } return fp; }
[1] https://stackoverflow.com/questions/30593964/how-to-access-android-lollipop-documentfile-files-via-ndk/31677287
[2] https://developer.android.com/guide/topics/providers/document-provider.html?hl=zh-cn
[2020元旦礼物]《看雪论坛精华17》发布!(补齐之前所有遗漏版本)!
最后于 21小时前 被devseed编辑 ,原因: