1:扫描SD卡内容方式一(不推荐)
- 扫描存储卡的第一种方式是:循环遍历所有SD卡的文件夹。这种方式好处是实时查询最新的内容,但是很慢,不推荐使用这种方式。由于比较耗时,普通用户大概扫描结束大概需要 30s 左右的时间,所以要开启子线程去查询。代码示例如下:
注意首先要申请动态存储权限
static class SearchTask extends AsyncTask<Void, Void, List<TemplateInfoBean>> {
volatile List<TemplateInfoBean> fileList = new ArrayList<>();
private WeakReference<SearchTemplateActivity> weakReference;
SearchTask(WeakReference<SearchTemplateActivity> weakReference) {
this.weakReference = weakReference;
}
@Override
protected List<TemplateInfoBean> doInBackground(Void... voids) {
long startTime = System.currentTimeMillis();
try {
// 从根目录开始查询
File file = Environment.getExternalStorageDirectory();
File[] files = file.listFiles();
if (files == null) {
return fileList;
}
List<File> list = Arrays.asList(files);
if (list.size() > 100) {
// 每100个文件夹开启一个线程,此处线程不用太多
List<List<File>> lists = fixedGrouping(list, 100);
List<Runnable> runnableList = new ArrayList<>();
for (int i = 0; i < lists.size(); i++) {
int j = i;
Runnable runnable = new Runnable() {
@Override
public void run() {
List<File> list1 = lists.get(j);
for (File file1 : list1) {
getAllFiles(file1, fileList);
}
}
};
runnableList.add(runnable);
}
final ExecutorService executorService = Executors.newFixedThreadPool(lists.size());
for (Runnable runnable : runnableList) {
executorService.execute(runnable);
}
executorService.shutdown();
//等待线程池中的所有线程运行完成
while (true) {
if (executorService.isTerminated()) {
break;
}
}
} else {
getAllFiles(file, fileList);
}
} catch (Exception e) {
LogUtils.error(e.toString());
}
LogUtils.error("time:" + (System.currentTimeMillis() - startTime));
return fileList;
}
@Override
protected void onPostExecute(List<TemplateInfoBean> templateInfoBeans) {
// 返回数据刷新适配器列表
weakReference.get().refreshList(templateInfoBeans);
}
@Override
protected void onCancelled() {
// 取消状态
}
}
/**
* 遍历扫描
*/
public static void getAllFiles(File f, List<TemplateInfoBean> fileList) {
if (!f.exists()) {//判断路径是否存在
return;
}
File[] files = f.listFiles();
if (files == null) {//判断权限
return;
}
for (File file : files) {
// xls xlsx 如果是Excel就添加
if (file.isFile() && (file.getName().endsWith(".xls") || file.getName().endsWith(".xlsx"))) {
LogUtils.error(file.getAbsolutePath());
// 如果是指定文件
TemplateInfoBean bean = new TemplateInfoBean(file.getAbsolutePath(), file.getName(), showLongFileSize(file.length()), file.lastModified());
fileList.add(bean);
} else if (file.isDirectory()) {//查询子目录
getAllFiles(new File(file.getAbsolutePath()), fileList);
}
}
}
/**
* 每组 n 条,等分集合
*/
public static <T> List<List<T>> fixedGrouping(List<T> source, int n) {
if (null == source || source.size() == 0 || n <= 0)
return null;
List<List<T>> result = new ArrayList<List<T>>();
int sourceSize = source.size();
int size = (source.size() / n) + 1;
for (int i = 0; i < size; i++) {
List<T> subset = new ArrayList<T>();
for (int j = i * n; j < (i + 1) * n; j++) {
if (j < sourceSize) {
subset.add(source.get(j));
}
}
result.add(subset);
}
return result;
}
/**
* 字节转换
*/
public static String showLongFileSize(long length) {
if (length >= 1048576) {
return (length / 1048576) + "MB";
} else if (length >= 1024) {
return (length / 1024) + "KB";
} else {
return length + "B";
}
}
class TemplateInfoBean(val path: String, val name: String, val size: String, val time: Long) : Comparable<TemplateInfoBean> {
var select = false
override fun compareTo(other: TemplateInfoBean): Int {
return (other.time - time).toInt()
}
}
- 此方式随着手机app增多会变慢,如果手机性能差会有一些卡顿。不推荐
2:扫描SD卡内容方式二(推荐)
- 因为上面的方式扫描时间慢,效率低所有为了优化满足需求,进行了优化第二版如下所示:
原理是:Android系统会在自己内部维护一个数据库,存储了手机内部的各种媒体文件的信息。我们使用ContentResolver去查询数据库的内容。
// 还是这个Task
static class SearchTask extends AsyncTask<Void, Void, List<TemplateInfoBean>> {
// 要查询的表的列 这里有 地址 大小 时间等
final String[] DOC_PROJECTION = {
MediaStore.Files.FileColumns.DATE_MODIFIED,
MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.SIZE,
MediaStore.Files.FileColumns.TITLE,
};
volatile List<TemplateInfoBean> fileList = new ArrayList<>();
private WeakReference<SearchTemplateActivity> weakReference;
SearchTask(WeakReference<SearchTemplateActivity> weakReference) {
this.weakReference = weakReference;
}
@Override
protected List<TemplateInfoBean> doInBackground(Void... voids) {
try {
// 第一步 扫描内部存储的 Excel MediaStore.Files.getContentUri("external")就是获取外部存储的 Uri
// query 方法第一个参数是要扫描的 Uri,第二个参数是要查询的列,第三个参数是查询的
// projection是我们要查询的列;
// selection使我们自己的查询条件;
// 最后一个参数order是排序方式,默认可以传null
String selection1 = "(" + MediaStore.Files.FileColumns.DATA + " LIKE '%.xls%'" +
" or " + MediaStore.Files.FileColumns.DATA + " LIKE '%.xlsx%'" + ")";
final Cursor cursor = weakReference.get().getContentResolver().query(
MediaStore.Files.getContentUri("external"),
DOC_PROJECTION,
selection1,
null,
null
);
if (cursor != null) {
fileList.addAll(getDocumentFromCursor(cursor));
cursor.close();
}
} catch (Exception e) {
LogUtils.error(e.toString());
}
return fileList;
}
private ArrayList<TemplateInfoBean> getDocumentFromCursor(Cursor cursor) {
cursor.moveToFirst();
ArrayList<TemplateInfoBean> documents = new ArrayList<>();
while (cursor.moveToNext()) {
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE));
String title = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.TITLE));
// 数据库查询的时间是秒为单位的
long lastModifiedTime = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED));
if (path != null) {
TemplateInfoBean document = new TemplateInfoBean(path, title, showLongFileSize(size), lastModifiedTime * 1000);
if (!documents.contains(document)) {
documents.add(document);
}
}
}
return documents;
}
@Override
protected void onPostExecute(List<TemplateInfoBean> templateInfoBeans) {
weakReference.get().refreshList(templateInfoBeans);
}
// 作用:将异步任务设置为:取消状态
@Override
protected void onCancelled() {
}
}
- 此种方式存在的问题
系统只会在开机的时候更新一次数据库,开机以后添加进来的文件是不会出现在数据库记录中,所有有了下面两个点,如何通知系统去更新数据库
3:通知系统更新媒体数据库文件方式一
- 直接上代码发送广播 (我的手机版本是 8.0的,据说低版本的 uri获取方式需要变一下)
// 我这里默认是扫描所有文件夹,可以具体根据自己的需求更新自己的文件夹
Uri uri = Uri.parse("file://" + Environment.getExternalStorageDirectory());
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,uri);
sendBroadcast(intent);
这样系统就会去指定地址去查询更新数据库了
4:通知系统更新媒体数据库文件方式二(带回调)
- 当想监听系统扫描是否结束的时候需要监听怎么办呢?用下面方法:
此方法需要注意的是:onScanCompleted 是不是在主线程所以更新UI要注意会有如下提示
Only the original thread that created a view hierarchy can touch its views.
所以我使用了Handler
String[] path = new String[]{Environment.getExternalStorageDirectory().getAbsolutePath()};
String[] mimeType = new String[]{"application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/x-excel", "application/x-msexcel"};
// 此方法第一个参数最好用 ApplicationContext避免内存泄漏,因为系统是开启了一个服务去查询,如果查询未结束前退出当前页面会发生内存泄漏
// 第二个参数是需要扫描的路径
// 第三个参数是 mime_type 根据自己的去求去更改,我的是指定 Excel 文件的 mime_type,如果全部可以 */*。具体可以自行百度
MediaScannerConnection.scanFile(this.getApplicationContext(), path, mimeType, new MediaScannerConnection.MediaScannerConnectionClient() {
@Override
public void onMediaScannerConnected() {
// 连接成功回调
}
@Override
public void onScanCompleted(String path, Uri uri) {
// 扫描完成回调 此时是在 Binder一个线程中
handler.sendEmptyMessage(0);
}
});
Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
// todo
}
};
------------------------------到此结束 -----------------------------------
5:新增备注
由于一些机型通知扫描的方法可能会有问题。如果扫描使用java遍历的方式即使采用多线层也会很慢,可以考虑采用 NDK 的方式会比java的方式快很多。