Android N Android OデフォルトMTPモードリアルタイムファイルスキャン

23209 ワード

背景
最近、お客様からのフィードバックは、当社の設備がサムスンの機械のように、usbがパソコンに接続するときはデフォルトでmtpモードであることを望んでいます.同時に、パソコンが携帯電話のファイルを見るときに一致しない問題(つまり、携帯電話で作成した新しいファイルやディレクトリ、パソコンではタイムリーに見ることができません)を解決することができます.
デマンドぶんかい
需要を分解してみましょう.実は2つの需要です.
1.usb接続パソコンのデフォルトmtpモード
2.リアルタイムファイルスキャン
需要実現構想
一般的に、もし需要がos測定を動かさなければ、私たちはできるだけosを動かさないことができます.
デフォルトのmtpモード:usbの挿入とusbの切断を監視することができ、usb線を挿入すると充電モードからmtpモードに切り替えることができます.
リアルタイムファイルスキャン:このニーズには、mtpモードに切り替えることをtriggerとして、フルスキャンを行う2つの実装があります.方式2は、ファイルcreateやdeleteがある場合に、そのファイルをスキャンするストレージ空間を監視することである.
1.デフォルトmtpモード
考え方:
つのブロードキャストAを登録して起動ブロードキャストを受け入れて、ブロードキャストAの中でstart 1つのサービス、サービスは動態的にブロードキャストを登録して“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 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システムに多くあります.例えば、Bluetooth、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
放送はUsbDeviceManagement.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の説明については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のプロジェクトのcompileSdkValersonは26、つまりandroid Oですから.だからNのapiは反射でしかできません.
その他の注意点
コードには、usb線を挿入するときにmtpモードに切り替えるだけであることを保証するフラグビットを追加する必要があります.mUsbModeInitというフラグビットがなければ、usbに接続した場合、手動で充電モードに切り替えても自分でmtpモードにジャンプします.これは明らかに私たちが望んでいるものではありません.
リアルタイムファイルスキャン:Mtpモードでフルスキャンをトリガー
考え方:
上記と同様に,usb stateの変化を傍受し,mtpモードに切り替わったと判断したとき,フルスキャンをトリガーした.スキャンが終わってからmtpモードに切り替えてパソコンにデバイスを表示するファイルシステムが必要かどうか疑問に思う人もいるかもしれませんが、実測は不要です.
実はネット上で比較的に通用するスキャンファイルのapiは1つは放送を送信することです
    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);
                    }
                });
}

しかし、この2つの方法はいずれも1つのファイルまたは複数のファイルをスキャンするしかありません.指定したディレクトリスキャンまたは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);
}
…
}

ここに興味のある人は、ロゴを変更して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 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 iterator = mObservers.keySet().iterator();
        while (iterator.hasNext()) {
            String key = iterator.next();
            mObservers.get(key).startWatching();
        }
    }

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

        Iterator 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);
            }
        }
    }
}

分析のまとめ:
コードにはまだ注目すべき点があります.
1つはFileObserverが傍受する場合、指定したディレクトリしか傍受できません.そのサブディレクトリは傍受できません.
ここで参考にしましたhttps://www.jianshu.com/p/65fb687d3458既存のディレクトリを反復的にリスニングし、新しいディレクトリもリスニングするという考え方です.
そして私たちは前に述べたACTIONです.MEDIA_SCANNER_SCAN_FILEはラジオやMediaScannerConnectionでスキャンを行います.この2つの方法はファイルに対してだけで、フォルダには効果がありません.パラメータがフォルダであっても、パソコンに接続して見たのは同名のファイルです.
例を挙げます.
Case 1:ディレクトリがあり、a 1ファイルを作成します.a 1ファイルパスに転送され、スキャンに成功します.
Case 2:フォルダではなくfolder 2というファイルを認識するfolder 2パスを作成します.
Case 3:新しいディレクトリfolder 1を作成し、新しいディレクトリにa 2ファイルを作成し、bファイルパスを入力し、スキャンに成功します.Folderも認識に成功した.
この3つの場合,コードには処理と注釈が施されている.
Case 1は、直接ブロードキャストまたはMediaScannerConnectionを呼び出せばOKです.
Case 2では、ファイルを作成し、スキャンが終了してから削除する方法を採用しています.ここで参考にしましたhttps://stackoverflow.com/questions/32637993/android-scanfile-on-empty-directory
Case 3では、ファイル管理クラスのapkに手動でディレクトリを追加し、ディレクトリにファイルを追加すると、このファイルのcreateを傍受することができます.しかし、コードで作成されたディレクトリとファイルの場合、ディレクトリ作成ok、ファイル作成okが表示され、新しいディレクトリの傍受が加えられ、新しいファイルのcreateが傍受されません.新しいディレクトリを見つけたら、スキャンしてこのバグを解決することができます.
まとめ
デフォルトのmtpモードでは、何も言うことはありません.上記の方法でいいです.
リアルタイムファイルスキャンの2つの方法:
いくつかの違いがありますが、mtpモードがフルスキャンをトリガーすると、既知のファイルマネージャがdoMediaScanを採用しているため、少し正道になります.
全体的な傍受は、理論的にはmtpモードよりも優れています.理論的には、1つのファイルを全面的に傍受すると、作成時に1回しかスキャンされませんが、mtpモードトリガはこのファイルを複数回スキャンする可能性があります.しかし、リスニングのオーバーヘッドは評価しにくい.結局、現在テストに使用されているマシンには反復的なディレクトリがあまりなく、ファイルの数も多くない.
全体的なリスニングは、mtpモードよりも利点があります.コンピュータに接続し、すでにmtpモードの場合、削除ファイルを作成すると、全体的なリスニングの方法はコンピュータに反映されますが、mtpモードのトリガはできません.
以上、私の個人プロジェクトで使用しているのはmtpモードがトリガする方法です.
 
Ok、大成功!皆さんに役に立つことを願っています.