前言
最近,用uniapp写一个app,其中有个功能,需要访问SDcard中的目录和文件,在查阅uniapp官方文档后,发现文档给出的api不支持app端,只能用html5+native.js去获取。于是,在踩了无数坑之后,就有了这篇记录。
安卓是如何实现的
安卓针不同的版本实现的方法有所不同, Android 6 (API 23) 之前应用的权限在安装时全部授予,运行时应用不再需要询问用户。在 Android 6.0 或更高版本对权限进行了分类,对某些涉及到用户隐私的权限可在运行时根据用户的需要动态授予
Android 10
- 需要在 AndroidManifest.xml 中添加 android.permission.WRITE_EXTERNAL_STORAGE 和 android.permission.READ_EXTERNAL_STORAGE 进行权限申请。
- application 中设置 android:requestLegacyExternalStorage="true"
- 然后需要动态的请求权限
Android 11
- 需要在 AndroidManifest.xml 中添加 android.permission.MANAGE_EXTERNAL_STORAGE
- 需要动态获取权限
- 参考 android 11 获取全部文件权限
- 参考 Android 10、11 存储完全适配
- 参考 Android Q中文件沙盒模式读写文件 08-16
- 参考 Android开发之 permission动态权限获取
- 参考 Android 获取某个文件夹下的所有文件
- 参考 Java--getAbsolutePath()获取绝对路径和相对路径getPath()getName()listFiles()
uniapp中几种实现方式
- 使用 web-view , 指向一个 html 文件, 在html文件里面用input实现
- 用 html5+和native.js
- 参考 HTML5+规范5+Specification
文档里需要用到的主要api介绍
- requestPermissions 获取授权 !!!!此api十分重要,如果不用这个api获取权限的话,就只能读取沙盒或公共媒体文件夹里的文件, requestPermissions 需要传入三个参数 第一个是权限数组,第二个成功回调方法,第三个是失败回调
- runtimeMainActivity 我原本以为之只是获取一个Activity实例后来才发现它相当于 Android中的 Context
- importClass 这个api是导入包的api 你可以到导入java包写原生
- newObject 有了这个api感觉可以将 importClass 丢一边去了,这个api是将导入和实例化合并了,第一次参数填要导入的包,第二个填实例化的需要传递的参数
- invoke 这个api是调用方法!!!十分重要
- IO IO模块管理本地文件系统,用于对文件系统的目录浏览、文件的读取、文件的写入等操作
实现
注:下面代码如果有更好的实现方式或那里写错了,欢迎各位大佬在评论区指正
-
打开文件管理器
// 获取应用主Activity实例对象 const MAIN = plus.android.runtimeMainActivity(); const INTENT = plus.android.importClass('android.content.Intent'); // 导入 Intent 类 const INTENT_OBJ = new INTENT(INTENT.ACTION_GET_CONTENT); INTENT_OBJ.addCategory(INTENT.CATEGORY_OPENABLE); // 创建分类 INTENT_OBJ.setType("*/*"); // 设置类型, 任意类型 image/* video/* .... // intent.putExtra(Intent.EXTRA_MIME_TYPES, 'image/*'); 设置多个类型 // intent.setDataAndType(mUri,"image/*"); MAIN.onActivityResult = (requestCode, resultCode, data) => { // ... 选择的文件data MAIN.startActivityForResult(INTENT_OBJ, 1);
-
获取外部存储权限
const permissionsList = [ "android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.READ_EXTERNAL_STORAGE" ]; plus.android.requestPermissions(permissionsList,(e)=>{ // ... 注: e里面包括了永久拒绝,拒绝,同意授权这些信息 })
-
判断安卓版本
let Build = plus.android.importClass('android.os.Build'); let isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
-
判断格式是否是uri类型的
let DocumentsContract = plus.android.importClass('android.provider.DocumentsContract'); // 判断是否是这个格式的url content://com.android.providers.media.documents/document/image%3A82482 let isSystemUri = DocumentsContract.isDocumentUri(MAIN, uri);
-
打开文件文件管理器选择文件返回绝对路径
/** * TODO 系统自带的文件管理器 中 最近/图片/视频。。。等直接获取 codid会是类型+文件id 没有之前的路径 需要检测添加 * https://blog.csdn.net/qq_43278826/article/details/101672670 * https://www.runoob.com/w3cnote/android-tutorial-intent-base.html * https://juejin.cn/post/7012108220982362149 * @method openFileManager 打开系统文件管理器 选择文件放回文件路径 * @description uniapp 没有提供打开安卓文件管理器的api 必须使用 input 或 plus * */ openFileManager: function() { // platform 系统 browserVersion 系统版本 const {platform, browserVersion} = uni.getSystemInfoSync(); if(platform == 'android') { const Activity = plus.android.runtimeMainActivity(); // 获取 Activity const Intent = plus.android.importClass("android.content.Intent"); // 导入 Intent let initen_new = new Intent(Intent.ACTION_GET_CONTENT, null); // 实例化 Intent 并允许 获取的是所有本地文件 可设置文件格式用于限制 initen_new.setType("*/*"); // 设置类型 */* 无类型限制 initen_new.addCategory(Intent.CATEGORY_OPENABLE); // Activity.startActivityForResult(initen_new, 1); // 启动Activity 打开系统的文件管理器 /** * onActivityResult 安卓中 用于从其他页面返回时带回数据 * */ Activity.onActivityResult = (requestCode, resultCode, data) => { // console.log("打开文件管理器后选择的文件返回了", requestCode, resultCode, data); // 打开文件管理器后选择的文件返回了 const Uri = data.getData(); plus.android.importClass(Uri); const DocumentsContract =plus.android.importClass("android.provider.DocumentsContract"); const Build = plus.android.importClass('android.os.Build'); let isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; // 判断安卓版本是否大于 4.4 /** * uri=content://com.android.providers.media.documents/document/image%3A293502 4.4以后 * uri=file:///storage/emulated/0/temp_photo.jpg * uri=content://media/external/images/media/193968 * * uri=content://media/external/images/media/13 4.4以前 */ if(DocumentsContract.isDocumentUri(Activity, Uri)){ // 获取文件类型和id const DocId = DocumentsContract.getDocumentId(Uri); const [Type, Id] = DocId.split(":"); // 解析出数字格式的id // console.log("文件类型和id",Type, Id); let authority = Uri.getAuthority(); // if(authority == "com.android.providers.media.documents") { const MediaStore = plus.android.importClass('android.provider.MediaStore'); let contentUri = null; let selection = "_id=?"; let selectionArgs = [Id]; // TODO 有些文件夹下文件无法获取uri应当是此处问题 switch(Type) { case 'image': contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI ;break; case 'video': contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI ;break; case 'audio': contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI ;break; } this.getDataColumn(Activity, contentUri, selection, selectionArgs); } else if(authority == "com.android.providers.downloads.documents"){ const ContentUris = plus.android.importClass('android.content.ContentUris'); let contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), parseInt(DocId));// 此处需要将文件id转换成long类型的 不然返回的是null this.getDataColumn(Activity, contentUri); } else if(authority == "com.android.externalstorage.documents"){ const Environment = plus.android.importClass('android.os.Environment'); if("primary" == Type) { console.log("类型为primary的文件地址:", `${Environment.getExternalStorageDirectory()}/${Id}`); } else { const System = plus.android.importClass('java.lang.System'); console.log("类型非primary的文件地址",`${System.getenv("SECONDARY_STORAGE")}/${Id}`); } } } else if("content" == Uri.getScheme()) { this.getDataColumn(Activity, Uri); } else if("file" == Uri.getScheme()) { console.log("文件路径:", uri.getPath()); } } } console.log("当前手机的系统是",platform); }, /** * uri转路径转换 * @method getDataColumn uri转路径转换 * @param {Obejct} activity 安卓的实例 * @param {Obejct} uri 获取到的文件地址 * @param {String} selection * @param {Array} selectionArgs 文件的id数组 * */ getDataColumn: function(activity, uri, selection = null, selectionArgs = null) { /** * 官方,提供了两个, 用来将绝对路径和平台路径互相转换的api * plus.io.convertAbsoluteFileSystem(path) 将平台绝对路径转换成本地URL路径 * plus.io.convertLocalFileSystemURL(url) 本地URL路径转换成平台绝对路径 * 绝对路径符合各平台文件路径格式,通常用于Native.JS调用系统原生文件操作API,也可以在前面添加“file://”后在html页面中直接使用。 */ plus.android.importClass(activity.getContentResolver()); let cursor = activity.getContentResolver().query(uri, ['_data'], selection, selectionArgs, null); plus.android.importClass(cursor); if (cursor != null && cursor.moveToFirst()) { let column_index = cursor.getColumnIndexOrThrow('_data'); let result = cursor.getString(column_index) cursor.close(); uni.getFileInfo({ filePath: result, success: (res) => { console.log(res); } }) return result; } return null; }
-
获取用户所有以安装程序
/** * @method getAllApply 获取用户所有已安装程序 * */ getAllApply: function() { const main = plus.android.runtimeMainActivity(); // 此处相当于 context let pManager = plus.android.invoke(main, 'getPackageManager'); let pInfo = plus.android.invoke(pManager, 'getInstalledPackages', 0); let total = plus.android.invoke(pInfo, 'size'); // 遍历获取包名和应用名称 for (let i = 0; i < total; i++) { // 获取包名 let packName = plus.android.getAttribute(plus.android.invoke(pInfo, 'get', i), 'packageName'); // 获取包名对应的应用名 let obj = plus.android.invoke(pManager, 'getApplicationInfo', packName, 0); let appName = plus.android.invoke(pManager, 'getApplicationLabel', obj); console.log(packName, appName); } }
-
获取根目录
/** * @method getRootSDCar 获取手机外部存储目录 * @description 用于获取手机的外部存储目录 * */ getRootSDCar: function() { // .... 在只用之前一定要仙获取权限 不然只能访问到沙盒里的内容 // 获取root目录路径 const Environment = plus.android.importClass("android.os.Environment"); // getExternalStorageDirectory 获取外部存储目录即 SDCard // getRootDirectory 获取 Android 的根目录 即系统主目录 let data = Environment.getExternalStorageDirectory(); let rootPath = plus.android.invoke(data, "getAbsolutePath"); console.log("根目录", rootPath); }
-
获取绝对路径
/** * @method getAbsolutePath 获取绝对路径 * */ getAbsolutePath: function() { /* StorageEventListener中有onStorageStateChanged()方法,当sd卡状态改变时, 此方法会调用,对各状态的判断一般会用到Environment类,此类中包含的有关sd卡状态的常量有: MEDIA_BAD_REMOVAL: 表明SDCard 被卸载前己被移除 MEDIA_CHECKING: 表明对象正在磁盘检查 MEDIA_MOUNTED: 表明sd对象是存在并具有读/写权限 MEDIA_MOUNTED_READ_ONLY: 表明对象权限为只读 MEDIA_NOFS: 表明对象为空白或正在使用不受支持的文件系统 MEDIA_REMOVED: 如果不存在 SDCard 返回 MEDIA_SHARED: 如果 SDCard 未安装 ,并通过 USB 大容量存储共享 返回 MEDIA_UNMOUNTABLE: 返回 SDCard 不可被安装 如果 SDCard 是存在但不可以被安装 MEDIA_UNMOUNTED: 返回 SDCard 已卸掉如果 SDCard 是存在但是没有被安装 */ const main = plus.android.runtimeMainActivity(); // 此处相当于 context const Build = plus.android.importClass('android.os.Build'); const Environment = plus.android.importClass("android.os.Environment"); let state = Environment.getExternalStorageState(); // 返回sd卡状态 let isState = plus.android.invoke(state,'equals', Environment.MEDIA_MOUNTED); let dir = null; if(isState) { if(Build.VERSION.SDK_INT >= 29) { /* DIRECTORY_MUSIC 音乐存放 DIRECTORY_PODCASTS 系统广播 DIRECTORY_RINGTONES 系统铃声 DIRECTORY_ALARMS 系统提醒铃声 DIRECTORY_NOTIFICATIONS 系统通知铃声 DIRECTORY_PICTURES 图片存放 DIRECTORY_MOVIES 电影存放 DIRECTORY_DOWNLOADS 下载 DIRECTORY_DCIM 相机拍摄照片和视频 */ // dir = main.getExternalFilesDir(Environment.DIRECTORY_MUSIC) // 获取音乐; // dir = main.getDataDir(); // > data/形式的路径 // getDataDirectory dir = main.getExternalFilesDir(null); // 获取当前app下面的flies文件夹 this.path = plus.android.invoke(dir,"getAbsolutePath"); // 获取绝对路径 console.log("获取此应用下Flies文件夹路径",plus.android.invoke(dir,"getAbsolutePath")); /* // 获得父目录 this.filePath = plus.android.invoke(dir, "getParentFile"); console.log("父目录",plus.android.invoke(this.filePath, "getName")); // 拥有的文件 let child = plus.android.invoke(dir, "listFiles"); for(let i = 0 ;i<child?.length;i++){ this.childPath.push(plus.android.invoke(child[i], "getAbsolutePath")) console.log("是子文件",plus.android.invoke(child[i], "getAbsolutePath")); } */ } else { dir = Environment.getRootDirectory() } } },
-
通过绝对路径获取此路径下所有子文件 HTML5+ 实现
plus.io.resolveLocalFileSystemURL(`/storage/emulated/0/`,(metadata)=>{ // ... 需要获取权限 // metadata.isDirectory // 判断是是否是文件夹 // metadata.isFile();//判断是是否是文件 let directoryReader = metadata.createReader(); // 创建一个目录对象 获取下面的子文件 directoryReader.readEntries((entries)=>{ for (var i = 0; i < entries.length; i++) { console.log("文件信息:" + entries[i].name); } }, (err)=>{})
-
通过绝对路径获取此路径下所有子文件 偏向与原生 实现
getAllTheChildFiles: function(path){ this.childPath = [] const main = plus.android.runtimeMainActivity(); // 此处相当于 context const Build = plus.android.importClass('android.os.Build'); // 判断 sd卡状态 const Environment = plus.android.importClass("android.os.Environment"); const state = Environment.getExternalStorageState(); const isState = plus.android.invoke(state,'equals', Environment.MEDIA_MOUNTED); if(isState) { // 调用java File包实现 // let File = plus.android.importClass("java.io.File"); // let File_new = new File(metadata.toLocalURL()); const File = plus.android.newObject("java.io.File", `${path}`); // 导入包并new这个类 const exists = !plus.android.invoke(File, "exists"); // 判断路径是否存在 if(exists) return; const listFiles = plus.android.invoke(File, "listFiles");// 获取子文件列表 this.filePath = `${path}` for(let i = 0; i<listFiles.length;i++){ const name = `${plus.android.invoke(listFiles[i], "getName")}`; const isFile = plus.android.invoke(listFiles[i], "isDirectory"); } } }