Android N Android O 默认MTP模式 实时文件扫描

背景

最近客户那边反馈需求希望我司的设备能像三星的机器一样,usb连接电脑的时候默认是mtp模式,同时可以解决电脑查看手机上的文件有时候不一致的问题(也就是手机上创建的新文件或者目录,电脑上不能及时看到)。

需求分解

需求我们分解一下,其实是两个需求。

1.usb连接电脑默认mtp模式

2.实时文件扫描

需求实现思路

一般来说,如果需求可以不动os测实现我们尽量不去动os。

默认mtp模式:我们可以监测插入usb和断开usb,插入usb线的时候就把从充电模式切换到mtp模式。

实时文件扫描:这个需求其实有两种实现,方式一是以切换到mtp模式为trigger,来做一次全盘扫描。方式二是对存储空间进行监视,当有文件create或者delete的时候,去扫描该文件。

1.默认mtp模式

思路:

注册个广播A接受开机广播,广播A中start一个Service,Service动态注册广播来接受"android.hardware.usb.action.USB_STATE",判断好插入usb线的条件,保证在插入usb线后跳到mtp模式。

代码:

private boolean mUsbModeInit=true;

IntentFilter filter = new IntentFilter();
filter.addAction("android.hardware.usb.action.USB_STATE");
registerReceiver(mReceiver, filter, null, null);

private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals("android.hardware.usb.action.USB_STATE")){
                boolean connected = intent.getExtras().getBoolean("connected");
                boolean configured = intent.getExtras().getBoolean("configured");
                boolean unlocked = intent.getExtras().getBoolean("unlocked");
                if(!connected&&!configured){
                    mUsbModeInit=true;
                }
                if (connected&&configured&&!unlocked&&mUsbModeInit){
                    UsbManager usbManager = (UsbManager) getSystemService(UsbManager.class);
                    if(Build.VERSION.SDK_INT<Build.VERSION_CODES.O){
                        try {
                            Class<?> clazz=Class.forName("android.hardware.usb.UsbManager");
                            Method method=clazz.getMethod("setCurrentFunction", String.class);
                            method.invoke(usbManager,UsbManager.USB_FUNCTION_MTP);
                            Method method1=clazz.getMethod("setUsbDataUnlocked", boolean.class);
                            method1.invoke(usbManager,true);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }else {
                        usbManager.setCurrentFunction(UsbManager.USB_FUNCTION_MTP, true);
                    }
                    mUsbModeInit=false;
                }
            }
}

分析总结:

我们注册广播来监听"android.hardware.usb.action.USB_STATE",顾名思义,只要usb的state发生变化,系统就会发出该广播。

有兴趣的朋友可以实测一下,当我们插入usb线或断开usb线的时候,该广播会触发多次。这种state change的广播在android系统中有很多,比如蓝牙,wifi的打开和关闭也是一样的。打开和关闭也不仅仅是发了一次广播。

所以,在广播中根据广播携带的内容来做判断就显得至关重要了,毕竟我们只希望我们的代码只执行一次。

源码分析

我们去追一追源码,涉及的相关类有:

frameworks/base/core/java/android/hardware/usb/UsbManager.java

frameworks/base/services/usb/java/com/android/server/usb/UsbDeviceManager.java

frameworks/base/services/core/java/com/android/server/connectivity/Tethering.java

广播是在UsbDeviceManager.java里面发出来的:

private void updateUsbStateBroadcastIfNeeded(boolean configChanged) {
    // send a sticky broadcast containing current USB state
    Intent intent = new Intent(UsbManager.ACTION_USB_STATE);
    intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
            | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
            | Intent.FLAG_RECEIVER_FOREGROUND);
    intent.putExtra(UsbManager.USB_CONNECTED, mConnected);
    intent.putExtra(UsbManager.USB_HOST_CONNECTED, mHostConnected);
    intent.putExtra(UsbManager.USB_CONFIGURED, mConfigured);
    intent.putExtra(UsbManager.USB_DATA_UNLOCKED,
            isUsbTransferAllowed() && mUsbDataUnlocked);
    intent.putExtra(UsbManager.USB_CONFIG_CHANGED, configChanged);

    if (mCurrentFunctions != null) {
        String[] functions = mCurrentFunctions.split(",");
        for (int i = 0; i < functions.length; i++) {
            final String function = functions[i];
            if (UsbManager.USB_FUNCTION_NONE.equals(function)) {
                continue;
            }
            intent.putExtra(function, true);
        }
    }

    // send broadcast intent only if the USB state has changed
    if (!isUsbStateChanged(intent) && !configChanged) {
        if (DEBUG) {
            Slog.d(TAG, "skip broadcasting " + intent + " extras: " + intent.getExtras());
        }
        return;
    }

    if (DEBUG) Slog.d(TAG, "broadcasting " + intent + " extras: " + intent.getExtras());
    mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
    mBroadcastedIntent = intent;
}

可以看出这个广播携带了很多extra。而且这个广播是黏性广播,这意味着你在注册broadcastReceiver后马上就能收到一次。所以即使我们插着usb线开机,没有插入usb线的操作去trigger,我们注册广播后依然可以把usb模式转换为mtp模式。

关于extras的解释我们在Tethering.java可以一探究竟:

它的handleUsbAction方法中有注释提到:

            // There are three types of ACTION_USB_STATE:
            //
            //     - DISCONNECTED (USB_CONNECTED and USB_CONFIGURED are 0)
            //       Meaning: USB connection has ended either because of
            //       software reset or hard unplug.
            //
            //     - CONNECTED (USB_CONNECTED is 1, USB_CONFIGURED is 0)
            //       Meaning: the first stage of USB protocol handshake has
            //       occurred but it is not complete.
            //
            //     - CONFIGURED (USB_CONNECTED and USB_CONFIGURED are 1)
            //       Meaning: the USB handshake is completely done and all the
            //       functions are ready to use.

这样就一目了然了。这里我们比较关注的是DISCONNECTED和CONFIGURED。

                boolean connected = intent.getExtras().getBoolean("connected");
                boolean configured = intent.getExtras().getBoolean("configured");
                boolean unlocked = intent.getExtras().getBoolean("unlocked");

connected和configured配合可以表明DISCONNECTED和CONFIGURED状态。

Unlocked可以表明是否是mtp模式。

Android O与N的适配

代码里面还有一个注意点是android不同版本间的适配,android O上是用的

UsbManager usbManager = (UsbManager) getSystemService(UsbManager.class);
usbManager.setCurrentFunction(UsbManager.USB_FUNCTION_MTP, true);

而android N上面则是

UsbManager usbManager = (UsbManager) getSystemService(UsbManager.class);
usbManager.setCurrentFunction(UsbManager.USB_FUNCTION_MTP);
usbManager. setUsbDataUnlocked (true);

因为O上api变了,所以直接调用会编译不过。因为我这边android studio的project的compileSdkVersion是26,也就是android O。所以N的api只能用反射来做了。

其它注意点

代码里面我们需要添加一个标志位,保证只会在插入usb线的时候切换成mtp模式。如果没有mUsbModeInit这个标志位,我们在连接usb的情况下,手动切换到充电模式也会自己跳到mtp模式,这显然不是我们想要的。

实时文件扫描:Mtp模式触发全盘扫描

思路:

跟上文一样,我们监听usb state的变化,当判断切换到mtp模式的时候,触发一次全盘扫描。可能会有人疑惑是否需要等扫描结束后再切换到mtp模式在电脑上显示出设备的文件系统,实测不用。

其实网上比较通用的扫描文件的api一个是发送广播

    public void scanPath(String path) {
        mIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        mIntent.setData(Uri.fromFile(new File(path)));
        mContext.sendBroadcast(mIntent);
}

一个是用MediaScannerConnection

    public void mediaScan(File file) {
        MediaScannerConnection.scanFile(this,
                new String[]{file.getAbsolutePath()}, null,
                new MediaScannerConnection.OnScanCompletedListener() {
                    @Override
                    public void onScanCompleted(String path, Uri uri) {
                        Log.v("MediaScanWork", "file " + path
                                + " was scanned seccessfully: " + uri);
                    }
                });
}

但是这两个方式都只能对一个文件或者多个文件进行扫描。如果想要对指定目录扫描或storage/emulated/0进行扫描就做不到了,除非咱们遍历所有文件去扫描。我们的优先目标肯定是最好有现成的简洁的api来调。

网上还有提到ACTION_MEDIA_MOUNTED,这个action在android O里面可谓名存实亡,并没有在源码里面找到相关的收这个广播的实际处理代码。

而ACTION_MEDIA_SCANNER_SCAN_DIR,在高版本的源码里面根本没这个action。

所以我们只能另辟蹊径,很偶然的机会,我在测试的过程中发现设备上自带的文件管理器(高通平台)创建的文件夹可以马上同步到电脑上,而我们使用File类的mkdir方法创建的文件夹却不能马上同步到电脑上。

追了下文件管理器的源码:

packages\apps\CMFileManager\src\com\cyanogenmod\filemanager\util\CommandHelper.java

createDirectory方法中有doMediaScan(context),把这个方法移到我们的代码中,果然生效了。

代码:

private boolean mUsbModeInit=true;

IntentFilter filter = new IntentFilter();
filter.addAction("android.hardware.usb.action.USB_STATE");
registerReceiver(mReceiver, filter, null, null);

private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals("android.hardware.usb.action.USB_STATE")){
                boolean connected = intent.getExtras().getBoolean("connected");
                boolean configured = intent.getExtras().getBoolean("configured");
                boolean unlocked = intent.getExtras().getBoolean("unlocked");
                if(!connected&&!configured){
                    mUsbModeInit=true;
                }
                if (connected&&configured&&unlocked&&mUsbModeInit){
                    doMediaScan(mContext);
                    mUsbModeInit=false;
                }
            }
}
public  void doMediaScan(Context context) {
    Bundle args = new Bundle();
    args.putString("volume", "external");
    Intent startScan = new Intent();
    startScan.putExtras(args);
    startScan.setComponent(new ComponentName("com.android.providers.media",
            "com.android.providers.media.MediaScannerService"));
    context.startService(startScan);
}

分析总结:

源码分析:

我们去追一下doMediaScan这个方法中的MediaScannerService具体是怎么进行全盘扫描的。

packages\providers\MediaProvider\src\com\android\providers\media\MediaScannerService.java

    public int onStartCommand(Intent intent, int flags, int startId) {
        while (mServiceHandler == null) {
            synchronized (this) {
                try {
                    wait(100);
                } catch (InterruptedException e) {
                }
            }
        }

        if (intent == null) {
            Log.e(TAG, "Intent is null in onStartCommand: ",
                new NullPointerException());
            return Service.START_NOT_STICKY;
        }

        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent.getExtras();
        mServiceHandler.sendMessage(msg);

        // Try again later if we are killed before we can finish scanning.
        return Service.START_REDELIVER_INTENT;
    }

onStartCommand里面把传进来的intent拿到,拿到intent的数据包装成一个Message,然后丢到消息队列里面去

   private final class ServiceHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
…..
else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
    // scan external storage volumes
    if (getSystemService(UserManager.class).isDemoUser()) {
        directories = ArrayUtils.appendElement(String.class,
                mExternalStoragePaths,
                Environment.getDataPreloadsMediaDirectory().getAbsolutePath());
    } else {
        directories = mExternalStoragePaths;
    }
}

if (directories != null) {
    if (false) Log.d(TAG, "start scanning volume " + volume + ": "
            + Arrays.toString(directories));
    scan(directories, volume);
    if (false) Log.d(TAG, "done scanning volume " + volume);
}
…
}

这里有兴趣的人,可以去改下log,然后可以看到scan的过程的耗时。

核心是scan方法

    private void scan(String[] directories, String volumeName) {
        Uri uri = Uri.parse("file://" + directories[0]);
        // don't sleep while scanning
        mWakeLock.acquire();

        try {
            ContentValues values = new ContentValues();
            values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
            Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);

            sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));

            try {
                if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
                    openDatabase(volumeName);
                }

                try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
                    scanner.scanDirectories(directories);
                }
            } catch (Exception e) {
                Log.e(TAG, "exception in MediaScanner.scan()", e);
            }

            getContentResolver().delete(scanUri, null, null);

        } finally {
            sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
            mWakeLock.release();
        }
}

Scan方法里面还持有了wakelock。原来是调用的MediaScanner对象的scanDirectories方法。MediaScanner类在以下路径

frameworks/base/media/java/android/media/MediaScanner.java

到这里,我们就暂时不用追了也真相大白了。

实时文件扫描:全盘监听

思路:

使用FileObserver监听storage/emulated/0这个目录,当有文件create或者delete,就触发一次扫描该文件。

代码:

private RecursiveFileObserver mRecursiveFileObserver;
public void onCreate() {
	mRecursiveFileObserver=new RecursiveFileObserver(FILE_OBSERVER_DIR,FileObserver.CREATE | FileObserver.DELETE,this);
	mRecursiveFileObserver.startWatching();
}
public void onDestroy() {
	mRecursiveFileObserver.stopWatching();
}
package com.honeywell.ezservice.utils;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Bundle;
import android.os.FileObserver;
import android.util.ArrayMap;
import android.util.Log;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;

public class RecursiveFileObserver extends FileObserver {
    Map<String, SingleFileObserver> mObservers;
    String mPath;
    int mMask;
    Context mContext;
    Intent mIntent;

    public RecursiveFileObserver(String path, Context context) {
        this(path, ALL_EVENTS, context);
    }

    public RecursiveFileObserver(String path, int mask, Context context) {
        super(path, mask);
        mPath = path;
        mMask = mask;
        mContext = context.getApplicationContext();
    }

    public void scanPath(String path) {
        mIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        mIntent.setData(Uri.fromFile(new File(path)));
        mContext.sendBroadcast(mIntent);
    }

    public void scanEmptyFolder(final Context context, File targetFile) {
        final File dummy = new File(targetFile, "init");
        try {
            dummy.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
        MediaScannerConnection.scanFile(context, new String[]{dummy.getAbsolutePath()}, null, new MediaScannerConnection.OnScanCompletedListener() {
            @Override
            public void onScanCompleted(String s, Uri uri) {
                // delete file and scan again (because file should not be displayed)
                dummy.delete();
                MediaScannerConnection.scanFile(context, new String[]{dummy.getAbsolutePath()}, null, null);
            }
        });
    }

    @Override
    public void startWatching() {
        if (mObservers != null)
            return;
        mObservers = new ArrayMap<>();
        Stack stack = new Stack();
        stack.push(mPath);

        while (!stack.isEmpty()) {
            String temp = (String) stack.pop();
            mObservers.put(temp, new SingleFileObserver(temp, mMask));
            File path = new File(temp);
            File[] files = path.listFiles();
            if (null == files)
                continue;
            for (File f : files) {
                // 递归监听目录
                if (f.isDirectory() && !f.getName().equals(".") && !f.getName()
                        .equals("..")) {
                    stack.push(f.getAbsolutePath());
                }
            }
        }
        Iterator<String> iterator = mObservers.keySet().iterator();
        while (iterator.hasNext()) {
            String key = iterator.next();
            mObservers.get(key).startWatching();
        }
    }

    @Override
    public void stopWatching() {
        if (mObservers == null)
            return;

        Iterator<String> iterator = mObservers.keySet().iterator();
        while (iterator.hasNext()) {
            String key = iterator.next();
            mObservers.get(key).stopWatching();
        }
        mObservers.clear();
        mObservers = null;
    }

    @Override
    public void onEvent(int event, String path) {
        int el = event & FileObserver.ALL_EVENTS;
        switch (el) {
            case FileObserver.ATTRIB:
                Log.i("RecursiveFileObserver", "ATTRIB: " + path);
                break;
            case FileObserver.CREATE:
                File file = new File(path);
                if (file.isDirectory()) {
                    Stack stack = new Stack();
                    stack.push(path);
                    while (!stack.isEmpty()) {
                        String temp = (String) stack.pop();
                        if (mObservers.containsKey(temp)) {
                            continue;
                        } else {
                            SingleFileObserver sfo = new SingleFileObserver(temp, mMask);
                            sfo.startWatching();
                            mObservers.put(temp, sfo);
                        }
                        File tempPath = new File(temp);
                        File[] files = tempPath.listFiles();
                        if (null == files)
                            continue;
                        for (File f : files) {
                            // 递归监听目录
                            if (f.isDirectory() && !f.getName().equals(".") && !f.getName()
                                    .equals("..")) {
                                stack.push(f.getAbsolutePath());
                            }
                        }
                    }
                }
                /*
                分几种情况
                1.已监听目录创建文件
                2.创建未监听目录
                3.创建未监听目录并同时创建文件(代码创建很快,这时候监听目录晚了,文件创建会监听不到)
                 */
                //potter add
                Log.i("RecursiveFileObserver", "CREATE: " + path);
                //case 1 begin
                if (file.isFile()) {
                    scanPath(path);
                }
                //case 1 end
                //如果是用代码创建的一个长目录带文件,而且目录不存在,这时候监听的时候,文件已经创建结束了,就会导致没有监听到,所以得遍历了scan一次
                if (file.isDirectory()) {
                    //case 3 begin
                    File[] files = file.listFiles();
                    for (File f : files) {
                        scanPath(f.getAbsolutePath());
                    }
                    //case 3 end
                    //case 2 begin
                    if (files.length == 0) {
                        scanEmptyFolder(mContext, file);
                    }
                    //case 2 end
                }
                //potter end
                break;
            case FileObserver.DELETE:
                Log.i("RecursiveFileObserver", "DELETE: " + path);
                break;
            case FileObserver.DELETE_SELF:
                Log.i("RecursiveFileObserver", "DELETE_SELF: " + path);
                break;
            case FileObserver.MODIFY:
                Log.i("RecursiveFileObserver", "MODIFY: " + path);
                break;
            case FileObserver.MOVE_SELF:
                Log.i("RecursiveFileObserver", "MOVE_SELF: " + path);
                break;
            case FileObserver.MOVED_FROM:
                Log.i("RecursiveFileObserver", "MOVED_FROM: " + path);
                break;
            case FileObserver.MOVED_TO:
                Log.i("RecursiveFileObserver", "MOVED_TO: " + path);
                break;
        }


    }

    class SingleFileObserver extends FileObserver {
        String mPath;

        public SingleFileObserver(String path) {
            this(path, ALL_EVENTS);
            mPath = path;
        }

        public SingleFileObserver(String path, int mask) {
            super(path, mask);
            mPath = path;
        }

        @Override
        public void onEvent(int event, String path) {
            if (path != null) {
                String newPath = mPath + "/" + path;
                RecursiveFileObserver.this.onEvent(event, newPath);
            }
        }
    }
}

分析总结:

代码里还是有一些需要关注的点的。

一个是FileObserver进行监听的时候,只能监听指定目录,它的子目录是监听不到了。

这里参考了https://www.jianshu.com/p/65fb687d3458,它的思路是对已有目录迭代进行监听,并对新建的目录也进行了监听。

然后我们是前面提到的ACTION_MEDIA_SCANNER_SCAN_FILE发广播或者MediaScannerConnection来做扫描,这两种方法都只是针对文件,对于文件夹达不到效果的,即使你传入的参数是一个文件夹,你连接电脑后看到的却是一个同名的文件。

举个例子:

Case1:已有目录,创建a1文件。传入a1文件路径,可以成功扫描。

Case2:创建新的目录folder2,传入folder2路径,电脑上识别的是一个叫folder2的文件,而不是文件夹。

Case3:创建新的目录folder1,新目录里面创建a2文件,传入b文件路径,可以成功扫描。Folder也能成功识别。

针对这三种情况,代码里面进行了处理和备注。

Case1,直接发广播或者调用MediaScannerConnection就ok。

Case2,我们采用创建一个文件,扫描结束后再删除的方式。这里参考了https://stackoverflow.com/questions/32637993/android-scanfile-on-empty-directory

Case3,这里要分两个情况,如果是手动在文件管理类的apk里面新增目录,然后目录里面添加文件是能够监听到这个文件的create的。但是如果是代码创建的目录和文件,会出现目录创建ok,文件创建ok,这时候新的目录的监听才加上,导致新增的文件的create没监听到。所以我们在发现新目录后就做下遍历操作来扫描下就可以解决这个bug。

总结

对于默认mtp模式,没什么好说的。用上面提到的方法即可。

而实时文件扫描的两种方式:

有一些区别,mtp模式触发全盘扫描就稍微路子正一点,因为已知的有的文件管理器就是采用的doMediaScan来做的。

而全盘监听呢,路子有点野,理论上是优于mtp模式触发的,因为理论上采用全盘监听一个文件只会在创建的时候扫描一次,而mtp模式触发则可能多次扫描这个文件。但是监听的开销又不好评估了,毕竟现在用来测试的机器并没有太多迭代的目录,文件个数也不算多。

全盘监听还比mtp模式触发有一个优势就是连接电脑并且已经是mtp模式下,如果这时候创建删除文件,全盘监听的方式是可以反映到电脑上去的,而mtp模式触发则做不到。

综上,我个人项目中使用的还是mtp模式触发的方式。

 

Ok,大功告成!希望对各位有用。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值