uniapp_01_实现打开文件管理

关于uniapp实现app端文件管理

  • 前言
  • 安卓是如何实现的
  • uniapp中几种实现方式
  • 文档里需要用到的主要api介绍
  • 实现

注: 可以先阅读 [h5+ io操作](https://www.html5plus.org/doc/zh_cn/io.html) 在看本文章, 当时没注意到io所以全调用的原生的包导致只能在安卓使用

前言

最近,用uniapp写一个app,其中有个功能,需要访问SDcard中的目录和文件,在查阅uniapp官方文档后,发现文档给出的api不支持app端,只能用html5+native.js去获取。于是,在踩了无数坑之后,就有了这篇记录。

安卓是如何实现的

安卓针不同的版本实现的方法有所不同, Android 6 (API 23) 之前应用的权限在安装时全部授予,运行时应用不再需要询问用户。在 Android 6.0 或更高版本对权限进行了分类,对某些涉及到用户隐私的权限可在运行时根据用户的需要动态授予

Android 10

  1. 需要在 AndroidManifest.xml 中添加 android.permission.WRITE_EXTERNAL_STORAGE 和 android.permission.READ_EXTERNAL_STORAGE 进行权限申请。
  2. application 中设置 android:requestLegacyExternalStorage="true"
  3. 然后需要动态的请求权限

Android 11

  1. 需要在 AndroidManifest.xml 中添加 android.permission.MANAGE_EXTERNAL_STORAGE
  2. 需要动态获取权限
  3. 参考 android 11 获取全部文件权限
  4. 参考 Android 10、11 存储完全适配
  5. 参考 Android Q中文件沙盒模式读写文件 08-16
  6. 参考 Android开发之 permission动态权限获取
  7. 参考 Android 获取某个文件夹下的所有文件
  8. 参考 Java--getAbsolutePath()获取绝对路径和相对路径getPath()getName()listFiles()

uniapp中几种实现方式

  1. 使用 web-view , 指向一个 html 文件, 在html文件里面用input实现
  2. 用 html5+和native.js
  3. 参考 HTML5+规范5+Specification

文档里需要用到的主要api介绍

  1. requestPermissions 获取授权 !!!!此api十分重要,如果不用这个api获取权限的话,就只能读取沙盒或公共媒体文件夹里的文件, requestPermissions 需要传入三个参数 第一个是权限数组,第二个成功回调方法,第三个是失败回调
  2. runtimeMainActivity 我原本以为之只是获取一个Activity实例后来才发现它相当于 Android中的 Context
  3. importClass 这个api是导入包的api 你可以到导入java包写原生
  4. newObject 有了这个api感觉可以将 importClass 丢一边去了,这个api是将导入和实例化合并了,第一次参数填要导入的包,第二个填实例化的需要传递的参数
  5. invoke 这个api是调用方法!!!十分重要
  6. IO IO模块管理本地文件系统,用于对文件系统的目录浏览、文件的读取、文件的写入等操作

实现

注:下面代码如果有更好的实现方式或那里写错了,欢迎各位大佬在评论区指正

  1. 打开文件管理器

     // 获取应用主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); 
    
  2. 获取外部存储权限

      const permissionsList = [
        "android.permission.WRITE_EXTERNAL_STORAGE",
        "android.permission.READ_EXTERNAL_STORAGE"
      ];
      plus.android.requestPermissions(permissionsList,(e)=>{
        // ... 注: e里面包括了永久拒绝,拒绝,同意授权这些信息
      })
    
  3. 判断安卓版本

        let Build = plus.android.importClass('android.os.Build');
        let isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
    
  4. 判断格式是否是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);
    
  5. 打开文件文件管理器选择文件返回绝对路径

      /**
       * 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;
      }
    
  6. 获取用户所有以安装程序

      /**
       * @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);
        }
      }
    
  7. 获取根目录

      /**
       * @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);
      }
    
  8. 获取绝对路径

      /**
       * @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()
          }
        }
      },
    
    
  9. 通过绝对路径获取此路径下所有子文件 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)=>{})
    
  10. 通过绝对路径获取此路径下所有子文件 偏向与原生 实现

    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");
        }
      }
    }
    
    
posted @ 2022-11-09 00:26  tsuru  阅读(2457)  评论(0编辑  收藏  举报