史上最全Android文件管理器技术方案细节

前言:

这些都是基于市面上开源的文件管理器源码阅读提炼出来的思路,应用市场上绝大多数的文件管理器核心功能的实现其实大同小异,开源项目以小米社区开放版为主。如思路有错误希望大家提出一起商讨更好的思路。

一.聚合分类列表显示方案:

1.写一个CategoryFragement显示列表界面
2.fragment中利用GridView+自定义CursorAdapter显示数据,gridView可以根据窗口宽度自动设置列数,初始化自定义CursorAdapter的时候类里面的newView可以通过逻辑判断动态设置GridView里面的Item布局达到网格列表和详情列表的切换
3.数据来源: 通过组装各个类别的URI查询系统数据库返回带有数据的cursor传入自定义的cursorAdapter
4.核心逻辑代码:
定义两个常量枚举装载所有分类类型和排序方法

 public enum FileCategory {
        Music, Video, Picture, Theme, Doc, Zip, Apk
    }
    public enum SortMethod {
        name, size, date, type
    }

根据不同种类的文件分类组装查询URI

 private Uri getContentUriByCategory(FileCategory cat) {
        Uri uri;
        String volumeName = "external";
        switch (cat) {
            case Theme:
            case Doc:
            case Zip:
            case Apk:
                uri = MediaStore.Files.getContentUri(volumeName);
                break;
            case Music:
                uri = MediaStore.Audio.Media.getContentUri(volumeName);
                break;
            case Video:
                uri = MediaStore.Video.Media.getContentUri(volumeName);
                break;
            case Picture:
                uri = MediaStore.Images.Media.getContentUri(volumeName);
                break;
            default:
                uri = null;
        }
        return uri;
    }

根据不同种类文件组装查询条件

private String buildSelectionByCategory(FileCategory cat) {
        String selection = null;
        switch (cat) {
            case Theme:
                selection = MediaStore.Files.FileColumns.DATA + " LIKE '%.mtz'";
                break;
            case Doc:
                selection = buildDocSelection();
                break;
            case Zip:
                selection = "(" + MediaStore.Files.FileColumns.MIME_TYPE + " == '" + Util.sZipFileMimeType + "')";
                break;
            case Apk:
                selection = MediaStore.Files.FileColumns.DATA + " LIKE '%.apk'";
                break;
            default:
                selection = null;
        }
        return selection;
    }

其中DOC文件类型比较多,额外写一个组装方法就不列出来了
组装查询默认排序规则:

 private String buildSortOrder(SortMethod sort) {
        String sortOrder = null;
        switch (sort) {
            case name:
                sortOrder = MediaStore.Files.FileColumns.TITLE + " asc";
                break;
            case size:
                sortOrder = MediaStore.Files.FileColumns.SIZE + " asc";
                break;
            case date:
                sortOrder = MediaStore.Files.FileColumns.DATE_MODIFIED + " desc";
                break;
            case type:
                sortOrder = MediaStore.Files.FileColumns.MIME_TYPE + " asc, " + MediaStore.Files.FileColumns.TITLE + " asc";
                break;
        }
        return sortOrder;
    }

根据以上准备工作进行查询:

private Cursor query(FileCategory fc, SortMethod sort) {
        Uri uri = getContentUriByCategory(fc);
        String selection = buildSelectionByCategory(fc);
        String sortOrder = buildSortOrder(sort);

    if (uri == null) {
        Log.e("liuquan", "invalid uri, category:" + fc.name());
        return null;
    }

    String[] columns = new String[]{
            MediaStore.Files.FileColumns._ID, MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns.SIZE, MediaStore.Files.FileColumns.DATE_MODIFIED
    };
    Log.d("liuquan", "fileCategoryHelper query / uri = " + uri + " selections = " + selection + " sortOrder = " + sortOrder);

    return mContext.getContentResolver().query(uri, columns, selection, null, sortOrder);
}

调用查询方法

case R.id.music:
cursor = query(FileCategory.Music, SortMethod.name);
break;
case R.id.video:
cursor = query(FileCategory.Video, SortMethod.name);
break;
case R.id.picture:
cursor = query(FileCategory.Picture, SortMethod.name);
break;
case R.id.theme:
cursor = query(FileCategory.Theme, SortMethod.name);
break;
case R.id.doc:
cursor = query(FileCategory.Doc, SortMethod.name);
break;
case R.id.zip:
cursor = query(FileCategory.Zip, SortMethod.name);
break;
case R.id.apk:
cursor = query(FileCategory.Apk, SortMethod.name);

这样数据就获取到了,调用自定义的cursorAdapter的changeCursor即可刷新数据。

二.文件列表显示方案


1.写一个FileFragement显示文件列表界面
2.fragment中利用GridView+自定义ArrayAdapter显示数据,gridView可以根据窗口宽度自动设置列数,
初始化自定义CursorAdapter的时候可以通过逻辑判断动态设置GridView里面的Item布局达到网格列表和详情列表的切换
3.数据来源:根据目录路径通过File类获取该目录下的文件列表,通过循环遍历文件列表数据,将文件List转化为我们自己封装的FIleInfo的List.
4.核心逻辑代码:
根据路径获得文件列表

ArrayList<FileInfo> mFileNameList = new ArrayList<FileInfo>();
File file = new File(path);
File[] listFiles = file.listFiles(null);
ArrayList<FileInfo> fileList = mFileNameList;
for (File child : listFiles) {
            // do not show selected file if in move state
            if(mFileViewInteractionHub.isMoveState()&& mFileViewInteractionHub.isFileSelected(child.getPath()))
                continue;

        String absolutePath = child.getAbsolutePath();
        if (Util.isNormalFile(absolutePath) && Util.shouldShowFile(absolutePath)) {
            FileInfo lFileInfo = Util.GetFileInfo(child,
                    mFileCagetoryHelper.getFilter(), Settings.instance().getShowDotAndHiddenFiles());
            if (lFileInfo != null) {

                fileList.add(lFileInfo);
            }
        }
    }

这样mFileNameList就添加了数据,数据是经过封装的FileInfo,然后通知适配器刷新数据mAdapter.notifyDataSetChanged(),这样就可以实现文件列表的显示。

三.文件列表的排序方案

1.聚合类文件列表的排序
思路:当用户更改排序规则的时候聚合类文件处理方式是改变查询的方法重新组装查询语句中的排序规则部分
核心代码:

 private String buildSortOrder(SortMethod sort) {
        String sortOrder = null;
        switch (sort) {
            case name:
                sortOrder = MediaStore.Files.FileColumns.TITLE + " asc";
                break;
            case size:
                sortOrder = MediaStore.Files.FileColumns.SIZE + " asc";
                break;
            case date:
                sortOrder = MediaStore.Files.FileColumns.DATE_MODIFIED + " desc";
                break;
            case type:
                sortOrder = MediaStore.Files.FileColumns.MIME_TYPE + " asc, " + MediaStore.Files.FileColumns.TITLE + " asc";
                break;
        }
        return sortOrder;
    }

注意:在SQL语句中最后接asc表示升序排列,desc表示降序排列,在项目中根据用户操作动态去改变这个字符串即可完成数据的升序和降序排列,查询完成之后会返回一个cursor,通知适配器改变cursor即可完成界面的刷新。
2.文件浏览列表的排序
思路:利用集合类Collections提供的sort方法对我们的数据集合进行排序,sort方法除了list数据源外还需要传入一个参数比较器,这里因为我们要根据文件名称(name),修改时间(modifyTime),大小(size)以及文件类型(type)进行升序和降序的排列,所以现在我们需要自定义4个比较器Comparator。
通过sort对数据源进行排序之后通知设配器刷新即可。
核心代码:

 private Comparator cmpSize = new FileComparator() {
        @Override
        public int doCompare(FileInfo object1, FileInfo object2) {
            return longToCompareInt(object1.fileSize - object2.fileSize);
        }
};

这里我们需要自定义一个FileComparator,然后初始化4个比较器的实力传入sort方法中完成排序。

四.文件操作方案

写一个文件操作帮助类来封装具体的操作,在需要完成操作的地方调对应的方法即可,有一点必须注意:操作完成之后一定要通知对应适配器刷新。
一)、文件夹的创建
思路:需要知道创建的地方,以及文件夹的名字,因为路径加名称组装后才能创建文件,所以可以写一个工具类去完成组装的工作。
代码:

 public boolean CreateFolder(String path, String name) {
        Log.v("liuquan", "CreateFolder >>> " + path + "," + name);
            File f = new File(Util.makePath(path, name));
            if (f.exists())
                return false;
            return f.mkdir();
        }

二)、重命名
思路:在列表中选中某个文件进行重命名,因为从列表中数据我们自己封装的FileInfo的list集合,我们可以直接获得该位置的FileInfo,因此重命名的实际操作方法可以以FileInfo以及新的名字当参数
代码:

public boolean Rename(FileInfo f, String newName) {
        if (f == null || newName == null) {
            Log.e(LOG_TAG, "Rename: null parameter");
            return false;
        }
    File file = new File(f.filePath);
    String newPath = Util.makePath(Util.getPathFromFilepath(f.filePath), newName);
    final boolean needScan = file.isFile();
    try {
        boolean ret = file.renameTo(new File(newPath));
        if (ret) {
            if (needScan) {
               //通知系统扫描文件
                mOperationListener.onFileChanged(f.filePath);
            }
            //通知系统扫描更新系统数据库内容
            mOperationListener.onFileChanged(newPath);
        }
        return ret;
    } catch (SecurityException e) {
        Log.e(LOG_TAG, "Fail to rename file," + e.toString());
    }
    return false;
}

*注意:*一定要通知系统扫描,不然你操作的文件信息是不会在系统数据库有记录的,这样聚合类里面就显示不了你操作的文件,虽然当你机器重启之后系统会自动去扫描,但是这个时间不可控,影响用户体验。
三)、删除文件
思路:同样也是通过列表位置获取对应的FileInfo对象进行操作

protected void DeleteFile(FileInfo f) {
        if (f == null) {
            Log.e(LOG_TAG, "DeleteFile: null parameter");
            return;
        }

    File file = new File(f.filePath);
    boolean directory = file.isDirectory();
    if (directory) {
        for (File child : file.listFiles(mFilter)) {
            if (Util.isNormalFile(child.getAbsolutePath())) {
                DeleteFile(Util.GetFileInfo(child, mFilter, true));
            }
        }
    }

    file.delete();

    Log.v(LOG_TAG, "DeleteFile >>> " + f.filePath);
}

*注意:*我们自己的文件浏览器逻辑是删除后将文件移动到回收站对应的目录,而不是真正删除,当用户清空回收站的时候才是真正的删除,所以删除文件的操作需要结合下面复制粘贴的方案才能完成。
四)、文件复制、粘贴、移动
思路:文件复制粘贴为一个操作,当用户点击copy后将所选择的文件装入一个list集合中,当用户点击粘贴的时候遍历集合将文件逐个copy到当前路径下
初始化一个ArrayList去装用户选择的待复制的FileInfo
ArrayList mCheckedFileNameList = new ArrayList();
用户选择后将用户选择的Item对应的FileInfo加入到这个集合中
当用户在某个目录下选择粘贴的时候获取路径开始真正执行复制到指定目录的逻辑
关键代码:
//粘贴操作

public boolean Paste(String path) {
        if (mCurFileNameList.size() == 0)
            return false;

    final String _path = path;
    asnycExecute(new Runnable() {
        @Override
        public void run() {
            for (FileInfo f : mCurFileNameList) {
                CopyFile(f, _path);
            }

            mOperationListener.onFileChanged(Environment
                    .getExternalStorageDirectory()
                    .getAbsolutePath());

             mCurFileNameList.clear();;
        }
    });

    return true;

}
//复制文件

private void CopyFile(FileInfo f, String dest) {
    if (f == null || dest == null) {
        Log.e(LOG_TAG, "CopyFile: null parameter");
        return;
    }

    File file = new File(f.filePath);
    if (file.isDirectory()) {

        // directory exists in destination, rename it
        String destPath = Util.makePath(dest, f.fileName);
        File destFile = new File(destPath);
        int i = 1;
        while (destFile.exists()) {
            destPath = Util.makePath(dest, f.fileName + " " + i++);
            destFile = new File(destPath);
        }
        Log.d(Util.MY_TAG," CopyFile FileOPerationHelper.FilenameFilter mFilter = "+mFilter);

        for (File child : file.listFiles(mFilter)) {
            if (!child.isHidden() && Util.isNormalFile(child.getAbsolutePath())) {
                CopyFile(Util.GetFileInfo(child, mFilter, Settings.instance().getShowDotAndHiddenFiles()), destPath);
            }
        }
    } else {
        String destFile = Util.copyFile(f.filePath, dest);
    }
    Log.v(LOG_TAG, "CopyFile >>> " + f.filePath + "," + dest);
}

如果在用户选择的Item中包含文件夹类型,则采用递归的形式去复制,最终是通过IO操作进行操作的。
移动文件就比较简单了,File类提供了renameTo这个方法,直接传入新的路径+文件名即可完成移动操作

private boolean MoveFile(FileInfo f, String dest) {
    Log.v(LOG_TAG, "MoveFile >>> " + f.filePath + "," + dest);

    if (f == null || dest == null) {
        Log.e(LOG_TAG, "CopyFile: null parameter");
        return false;
    }

    File file = new File(f.filePath);
    String newPath = Util.makePath(dest, f.fileName);
    try {
        return file.renameTo(new File(newPath));
    } catch (SecurityException e) {
        Log.e(LOG_TAG, "Fail to move file," + e.toString());
    }
    return false;
}

//设置图片为桌面壁纸

    Bitmap bitmap = BitmapFactory.decodeFile("/mnt/sdcard/RemixOS.jpg");
    WallpaperManager instance = WallpaperManager.getInstance(this);
    DisplayMetrics dm = new DisplayMetrics();
    getWindowManager().getDefaultDisplay().getMetrics(dm);
    int desiredMinimumWidth = dm.widthPixels;
    int desiredMinimumHeight = dm.heightPixels;
    Log.v("liuquan",""+desiredMinimumWidth);
    Log.v("liuquan",""+desiredMinimumHeight);
    instance.suggestDesiredDimensions(desiredMinimumWidth, desiredMinimumHeight);
    instance.setBitmap(bitmap);

五.收藏夹功能方案(快速访问)

思路:要完成这个功能,我们必须自己创建一个数据库file_explorer,新建一个表格favorite,表格只用插入名称(用于收藏夹或者快速访问列表显示的名称)和每个名称对应的绝对路径,这样在应用初始化的时候可以查询favirite表格,将收藏title以列表的形式显示出来,当用户点击列表内容的时候显示FileFragement以显示文件列表。
核心代码:
(创建数据库FavoriteDatabaseHelper帮助类的代码就不列出了,无非是数据库的增删改查,主要核心代码为数据的获取)
因为收藏夹用户可动态添加多个内容,系统也可初始化一些默认的路径,比较灵活,这里选择ListView+自定义ArrayAdapter的方式实现
而核心部分就是数据的获取,这里只列出数据获取的相关逻辑:
private ArrayList mFavoriteList = new ArrayList();
而这里我们需要封装一个FavoriteItem类来承载数据库对应的id、title、location以及FileInfo(通过location获取)

private FavoriteDatabaseHelper mFavoriteDatabase;
mFavoriteDatabase = new FavoriteDatabaseHelper(context, this);
 mFavoriteList.clear();

    Cursor c = mFavoriteDatabase.query();
    if (c != null) {
        while (c.moveToNext()) {
            FavoriteItem item = new FavoriteItem(c.getLong(0), c.getString(1), c.getString(2));
            item.fileInfo = Util.GetFileInfo(item.location);
            mFavoriteList.add(item);
        }
        c.close();
    }

同样,当mFavoriteList 数据填充完成之后通知适配器刷新即可。
当用户点击收藏(快速访问)列表的时候我们获取对应position的item的location路径,然后显示该目录下的文件列表FileFragement。
当用户添加或者删除快速访问的Item的时候直接通过FavoriteDatabaseHelper帮助类对数据库进行增加和删除即可,最后重新查询数据库并刷新列表。

六.文件搜索功能方案

思路:1、利用File类从根目录开始遍历所有文件夹和子目录来搜索文件
2、查询系统数据库,利用SQL的模糊匹配去对比获取的每一个文件Title与用户输入的关键字进匹配。
第一种方案运用递归的形式去遍历所有目录下的文件,再通过字符串的匹配去过滤,这种方式显然效率不高,影响用户体验,不过它也有他的优点,这种方式不依赖系统对文件数据库刷新情况,它是强制性去搜索所有的文件目录。
第二种方法显然效率高很多,但是强依赖系统文件数据库,这种情况有可能会造成文件的遗漏,比如某个第三方应用开发不够规范,下载保存了某个文件,但是没有通知数据库对该目录进行扫描更新,那么数据库里面就不会有这个文件相关的信息,在查询数据库的时候势必会遗漏。要解决这个问题理论上可以尝试我们的应用开启的时候可以开启一个线程去通知系统扫描整个机器的文件刷新系统数据库(亲测貌似还是有遗漏)。
关键代码:

 private static List searchKeyWord(Context context, String keyword) {

    List fileList = new ArrayList<>();
    ContentResolver resolver = context.getContentResolver();
    Uri uri = MediaStore.Files.getContentUri("external");
    Cursor cursor = resolver.query(uri,
            new String[]{MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns.SIZE},
            MediaStore.Files.FileColumns.TITLE + " LIKE '%" + keyword + "%'",
            null, null);
    if (cursor != null) {
        while (cursor.moveToNext()) {
            FileInfo fileInfo = new FileInfo();
            String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA));
            fileInfo.fileName = (path.substring(path.lastIndexOf("/") + 1));
            fileInfo.fileSize = (cursor.getLong(cursor.getColumnIndexOrThrow(
                    MediaStore.Files.FileColumns.SIZE)));
            fileList.add(fileInfo);
        }
    }
    cursor.close();
    return fileList;
}

遍历文件夹根据文件名字与关键字匹配(这个方案适用于在指定文件夹中搜索):

 public static List<File> searchFileInDir(File dir, String keywords) {
        if (dir == null || !isDir(dir)) return null;
        List<File> list = new ArrayList<>();
        File[] files = dir.listFiles();
        if (files != null && files.length != 0) {
            for (File file : files) {
//                (str.indexOf("ABC")!=-1
                if (file.getName().toUpperCase().contains(keywords.toUpperCase())) {
                    list.add(file);
                }
                if (file.isDirectory()) {
                    list.addAll(searchFileInDir(file, keywords));
                }
            }
        }
        return list;
    }

七.图片,视频,APK类文件缩略图缓存策略

文件管理器中图片,视频涉及到缩略图的显示,APK文件涉及到应用图标的显示,为了保证应用的性能,内存的优化格外重要,目前有两种方案:
1、软引用实现缓存,有一个必要条件就是它在系统内存不足时才会被释放,而从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠, 即在内存充足的情况下,它们指向的对象依然有可能被回收。如此,软引用Map做缓存并没有太大意义。
2、使用“主动“方式利用对内存控制的LruCache来处理缓存,其控制内存的方式是主动的,需要在内部记录当前缓存大小, 并与初始化时设置的max值比较,如果超过, 就将排序最靠前(即最近最少使用)的资源从LinkedHashMap中移除。这样就没有任何引用指向资源的内存空间了。该内存空间无人认领,会在GC时得到释放。
主要代码:
//LruCache初始化,一般为该应用运行最大内存的1//8

 public ImageDownloader() {
        long maxMemory = Runtime.getRuntime().maxMemory();
        int cacheSize = (int) (maxMemory / 8);
        lruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount();
            }
        };
    }

当我们要设置图片资源的时候首先从缓存中读取,
如果缓存中没有则通过原有逻辑去获取并加入到缓存中。
//apk文件图标的读取

public static Drawable getApkIcon(Context context, String apkPath) {
        PackageManager pm = context.getPackageManager();
        PackageInfo info = pm.getPackageArchiveInfo(apkPath,
                PackageManager.GET_ACTIVITIES);
        if (info != null) {
            ApplicationInfo appInfo = info.applicationInfo;
            appInfo.sourceDir = apkPath;
            appInfo.publicSourceDir = apkPath;
            try {
                return appInfo.loadIcon(pm);
            } catch (OutOfMemoryError e) {
                Log.e(LOG_TAG, e.toString());
            }
        }
        return null;
    }

//图片,视频文件缩略图的获取
//首先通过文件路径获取文件ID

public long getDbId(String path, boolean isVideo) {
        String volumeName = "external";
        Uri uri = isVideo ? Video.Media.getContentUri(volumeName) : Images.Media.getContentUri(volumeName);
        String selection = FileColumns.DATA + "=?";
        ;
        String[] selectionArgs = new String[] {
            path
        };

    String[] columns = new String[] {
            FileColumns._ID, FileColumns.DATA
    };

    Cursor c = mContext.getContentResolver()
            .query(uri, columns, selection, selectionArgs, null);
    if (c == null) {
        return 0;
    }
    long id = 0;
    if (c.moveToNext()) {
        id = c.getLong(0);
    }
    c.close();
    return id;

}
//通过ID到系统缩略图表中去找

private Bitmap getImageThumbnail(long id) {
 return Images.Thumbnails.getThumbnail(mContext.getContentResolver(), id, MICRO_KIND, null);
        }


private Bitmap getVideoThumbnail(long id) {
   return Video.Thumbnails.getThumbnail(mContext.getContentResolver(), id, MICRO_KIND, null);
        }

八.zip文件解压缩方案

思路:zip格式算是我们最常见的压缩文件格式,当然,也是最容易处理的压缩格式,jdk本身就支持对zip文件进行解压,既然是java平台的库,那么放在移动端上来运行,应该不会有太大问题的,毕竟zip解压对机器的性能要求不是很高。我们直接解压到当前目录为例。
一)解压
解压主要代码:

File dest = new File(fItem.getFile().getParentFile(), FileUtils.getNameWithoutExtension(fItem.getFile()));
                dest.mkdirs();
                new ExtractManager(context)
                        .setOnExtractFinishedListener(new ExtractManager.OnExtractFinishedListener() {

                        @Override
                        public void extractFinished() {
                            //TODO解压完成
                        }
                    })
                    .extract(fItem.getFile(), dest.getAbsolutePath());

因为解压文件过大的时候耗时比较长,这里我们起一个异步任务去处理在doInBackground中去处理解压过程,在onProgressUpdate中去处理progress的进度更新操作,处理完成之后在onPostExecute中去处理解压完成之后的操作。
实际解压代码:

zipfile = new ZipFile(archive);
                fileCount = zipfile.size();
                for (Enumeration e = zipfile.entries(); e.hasMoreElements(); ) {
                    ZipEntry entry = (ZipEntry) e.nextElement();
                    unzipEntry(zipfile, entry, destinationPath);
                    isExtracted++;
                    publishProgress();
                }

private void unzipEntry(ZipFile zipfile, ZipEntry entry,
                                String outputDir) throws IOException {
            if (entry.isDirectory()) {
                createDir(new File(outputDir, entry.getName()));
                return;
            }
            File outputFile = new File(outputDir, entry.getName());
            if (!outputFile.getParentFile().exists()) {
                createDir(outputFile.getParentFile());
            }
            BufferedInputStream inputStream = new BufferedInputStream(zipfile.getInputStream(entry));
            BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(outputFile));
            try {
                int len;
                byte[] buf = new byte[BUFFER_SIZE];
                while ((len = inputStream.read(buf)) > 0) {
                    outputStream.write(buf, 0, len);
                }
            } finally {
                outputStream.close();
                inputStream.close();
            }
            outputFile.setLastModified(entry.getTime());
        }

二)、压缩
同样也是将这个耗时操作起一个异步线程去执行

private void unzipEntry(ZipFile zipfile, ZipEntry entry,
                                String outputDir) throws IOException {
            if (entry.isDirectory()) {
                createDir(new File(outputDir, entry.getName()));
                return;
            }
            File outputFile = new File(outputDir, entry.getName());
            if (!outputFile.getParentFile().exists()) {
                createDir(outputFile.getParentFile());
            }
            BufferedInputStream inputStream = new BufferedInputStream(zipfile.getInputStream(entry));
            BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(outputFile));
            try {
                int len;
                byte[] buf = new byte[BUFFER_SIZE];
                while ((len = inputStream.read(buf)) > 0) {
                    outputStream.write(buf, 0, len);
                }
            } finally {
                outputStream.close();
                inputStream.close();
            }
            outputFile.setLastModified(entry.getTime());
        }

九.通过分享将文件保存到指定目录

一)、技术背景
在做项目的过程中需要实现文字和图片的分享,有两种方式:

  1. 使用android sdk中自带的Intent.ACTION_SEND实现分享。

  2. 使用shareSDK、友盟等第三方的服务。
    其实不管是哪种实现方式,其实基本原理都是通过intent的setAction来设置Intent.ACTION_SEND进行分享的。
    以分享一张图片为例:

    Intent intent = new Intent(Intent.ACTION_SEND);
    intent.setType(“image/*”);
    intent.putExtra(Intent.EXTRA_STREAM, uri);
    startActivity(intent);

这样就可以调起分享面板,选择通过什么方式分享,而这个分享面板中罗列出来的带有分享功能的模块是PackageManager的子类通过queryIntentActivities方法获取然后呈现的,底层实现是通过解析各个AndroidManifest中Activity配置的下action的类别来实现的,如果存在则表示该activity具有分享功能,然后将这个activity呈现在分享面板中供用户选择

通过下面这段代码可以将整个系统中所有具有分享功能的activity打印出来。

PackageManager pManager = mContext.getPackageManager();
mApps=pManager.queryIntentActivities(intent,PackageManager.COMPONENT_ENABLED_STATE_DEFAULT);
if (mApps != null) {
   for (ResolveInfo resolveInfo : mApps) {
    Log.v("liuquan2","packageName="resolveInfo.activityInfo.packageName);
Log.v("liuquan2", "activityName=" + resolveInfo.activityInfo.name);
      }
}
如下则为系统中所有能够处理分享的activity
08-17 02:14:52.555 4131-4131/com.example.liuquan.test 
V/liuquan2: packageName=com.android.bluetooth
activityName=com.android.bluetooth.opp.BluetoothOppLauncherActivity
packageName=com.google.android.apps.docs
activityName=com.google.android.apps.docs.drive.clipboard.SendTextToClipboardActivity
packageName=com.google.android.apps.docs    activityName=com.google.android.apps.docs.shareitem.UploadMenuActivity
packageName=com.google.android.apps.messaging activityName=com.google.android.apps.messaging.ui.conversationlist.ShareIntentActivity
packageName=com.google.android.gm
activityName=com.google.android.gm.ComposeActivityGmailExternal
packageName=com.example.sdcard
activityName=com.example.sdcard.TestActivity

当然这里需要说明一下:一个应用在调用分享功能的时候也分显示启动(指定通过什么分享)和隐式启动(不指定,让用户自己选择)
对于显示启动方式的我们没办法控制,其代码实现为:

Intent intent = new Intent();
ComponentName componentName = new ComponentName(
                            "com.example.sdcard",
                            "com.example.sdcard.TestActivity");
intent.setComponent(componentName);
intent.setType("image/*");  
intent.putExtra(Intent.EXTRA_STREAM, uri);
startActivity(intent);

我们只能对隐式启动这种方式进行操作。

二)、功能实现逻辑
基于上面的分析,要想我们自己定义的activity出现在供用户选择的面板中,只用我们在自己的activity中加入<action android:name="android.intent.action.SEND" />
如下:

<activity
        android:label="下载到我的电脑"
        android:name=".TestActivity">
        <intent-filter tools:ignore="AppLinkUrlError">
            <action android:name="android.intent.action.VIEW"/>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="*/*"/>
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>

这里设置的表示具有处理所有格式的分享文件的能力,"image/“表示只有处理图片的能力,应用只有分享图片的时候,分享面板中才会出现我们自己的activity。
我们需要实现对所有文件类型都可以进行本地保存,所以我们的mimeType设置成”
/*"即可。
那么当用户选择我们的activity进行保存的时候在我们自己的activity中接收到分享的数据怎么进行处理呢?具体实现如下:

Intent intent = getIntent();         
String action = intent.getAction();//获取action用于判断是否是分享
String type = intent.getType();//获取文件的mimetepy
 if (Intent.ACTION_SEND.equals(action) && type != null) {

        //if (type.startsWith("image/")) {
        //获取通过intent传递过来的文件uri
        Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
        //获取文件路径字符串,主要是为了后面通过该字符串获得文件名和文件后缀
    String path = ((Uri)intent.getParcelableExtra(Intent.EXTRA_STREAM)).getPath();            copyFilefromUri(uri,path);
    //}

}

private void copyFilefromUri(Uri imageUri, String path) {
    String absolutePath = Environment.getExternalStorageDirectory().getAbsolutePath();
    if (imageUri != null) {
//                imageView.setImageURI(imageUri);
            ContentResolver cr = getContentResolver();
            InputStream in = null;
            try {
                in = cr.openInputStream(imageUri);
                FileInputStream fileInputStream = (FileInputStream) in;
            //指定文件最终存储位置已经名称,这里只做测试,所以写死了,可以根据业务需求灵活定义,IO操作不要在main线程中执行
            File destFile = new File(absolutePath + "/" + getNameFromFilepath(path));
            if (destFile.exists()) {
                Log.d("liuquan", "文件已经存在啦");
                return;
            }
            FileOutputStream out = new FileOutputStream(destFile);
            int count = 102400;
            byte[] buffer = new byte[count];
            int read = 0;
            while ((read = fileInputStream.read(buffer, 0, count)) != -1) {
                out.write(buffer, 0, read);
            }
            in.close();
            out.close();

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

public static String getNameFromFilepath(String filepath) {
        int pos = filepath.lastIndexOf('/');
        if (pos != -1) {
            return filepath.substring(pos + 1);
        }
        return "";
    }

这样我们就将分享的文件复制保存到了我们指定的目录下了。
注意:其实这部分操作是对分享的文件进行了复制,其实分享的文件是本来就存在我们手机中的,由于安卓的特殊的存储机制,这个已经存在我们手机中的待分享的文件用户很难找到该文件,而通过这样处理之后,用户能够很方便找到该文件,提升用户易用性。

十.权限申请

以下为文件管理器可能涉及到的一些权限,必须在AndroidManifest.xml中配置以下权限
//获取当前WiFi接入的状态以及WLAN热点的信息

   <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 //访问网络连接,可能产生GPRS流量 
<uses-permission android:name="android.permission.INTERNET" />
//改变WiFi状态 
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
//允许程序读写外部存储,如SD卡上读写文件
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 //允许程序安装应用 (可能涉及)
    <uses-permission android:name="android.permission.INSTALL_PACKAGES" />
    //允许程序删除应用 
    <uses-permission android:name="android.permission.DELETE_PACKAGES" />
    //允许程序在手机屏幕关闭后后台进程仍然运
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    //获取网络信息状态,如当前的网络连接是否有效 
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    //挂载、反挂载外部文件系统
        <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"

这里要说明一下:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

这两个权限只在AndroidManifest中添加是没有用的,当系统读写SD卡操作时程序会出现crash,原因是没有权限,这就很奇怪了,明明配置了权限,但是还是报没有权限,最后发现Android6.0之后系统对权限的管理更加严格了,有一些权限不但要在AndroidManifest中添加(貌似不添加也可以),还要在应用运行的时候动态申请。文件读写为一个小组的权限,动态申请一个权限后,小组内的其他权限也会默认申请了。这里我们申请read权限

@SuppressLint("NewApi")
private void requestReadExternalPermission() {
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)
         != PackageManager.PERMISSION_GRANTED) {
         Log.d("liuquan", "READ permission IS NOT granted...");
       if (shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE)) {
                Log.d(TAG, "11111111111111");
            } else {
                requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0);
           Log.d(TAG, "222222222222");
            }
   } else {
            Log.d(TAG, "READ permission is granted...");
        }
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值