saf java_[原创]Android Storage Access Framework(SAF)框架实现外置SD卡的写入(JAVA层与JNI层HOOK)...

1. 前言

之前折腾了了一下MINE模拟器,发现SDL全是在JNI层fopen操作的,而安卓的SAF则是JAVA层通过DocumentFile和docUri来实现写入的。一种方法是通过去的File Descriptor然后传给JNI层,通过fdopen实现写入[1]。于是成功在MINE模拟器添加外置sd卡写入功能,详见我在贴吧布的 mine模拟器外置SD卡写入修复版。完整版源码我已经封装好了发布到github上,理论上通用。

5cc8da8050c43cf799d36bd804854979.png

2. JAVA层SAF核心代码

通过DocumentFile来实现写入,Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)发送请求docUri,然后onActivityResult来得到并储存docUri,通过SharedPreference来实现共享。已经封装在静态类SafFile.java中。

获取外置sd卡根目录DocumentFilepublic 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;

}

注意DocumentFile CreateFile好像不只能在本目录下创建,要一层一层往里面走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;

}

获得OutputStreampublic 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();

}

3.JNI层hook核心代码

这里用到了xhook架构,原理上是运行的时候来替换目标动态库的.got表到自己编译的函数地址,通过JNI来调用JAVA层我们写好通过SAF机制得到的文件描述符。

初始化要JNI中要调用的JAVA方法,class为"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!");

}

xhook架构的hook fopen等函数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);

}

fopen的hook, 调用java层我们写好的getFD再用fdopen文件可写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;

}

Reference

[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-1-14 22:12

被devseed编辑

,原因:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值