Posts RK-Android-Usb无法读取以及原理分析
Post
Cancel

RK-Android-Usb无法读取以及原理分析

前言

如果你不知道RK是啥玩意,请先google或者baidu (:з」∠)

环境

  1. RK3368
  2. Android 6.0.1

需要解决的问题

当设备接入U盘的后,RK全家桶都读不到U盘里的多媒体的资源,例如:mp4,mp3之类的. (不幸的是,这个功能是客户的刚需.)

解决方案

分析过程

1. 虽然RK全家桶都没有读到U盘里的数据,但是在文件夹管理器上却可以看到挂载上去的U盘,点击进去也能看到U盘里的资料,当时是判断为可能RK全家桶有点问题,回头就去下载了RK的RockVideoPlayer源码来分析.

2. 在RockVideoPlayer.java下就能看到这个播放器的基本原理了,在onCreate中调用了initLoader()

1
2
3
4
5
6
7
8
9
 public void initLoader() {
        int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE);
        if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
                    REQUEST_CODE_ASK_PERMISSIONS);
            return;
        }
        getLoaderManager().initLoader(0, null, this);
    }

再看看

1
 public class RockVideoPlayer extends Activity implements View.OnCreateContextMenuListener,DBUtils.Def,LoaderManager.LoaderCallbacks<Cursor>{}

基本就确定是用Android的CursorLoader来获得各种多媒体资源,CursorLoader也是有点小坑,以前我写过图片管理器也是用这个实现的. 再看看LoaderManager.LoaderCallbacks 这个接口的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 	@Override
	public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
		// TODO Auto-generated method stub
   	 	LOG("<-------------- onCreateLoader-------------->");
   	 	mSortOrder = MediaStore.Video.Media._ID;
	        StringBuilder where = new StringBuilder();
	        where.append(MediaStore.Video.Media._ID + " != ''");
	        where.append(" AND " + MediaStore.Video.Media.MIME_TYPE + " NOT LIKE 'audio%'");
		Log.i("kky", "onCreateLoader: "+ where.toString());
		Log.i("kky", "onCreateLoader: "+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
	    return new CursorLoader(RockVideoPlayer.this, MediaStore.Video.Media.EXTERNAL_CONTENT_URI, PROJECT, where.toString(), null, mSortOrder);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> arg0, Cursor arg1) {
    	if(arg1 == null || arg1.getCount() == 0){
    		toastNoVideo();
    	}
    	if(arg1 != null){
    		cursorLoader = true;
    		mVideoListAdapter.swapCursor(arg1);
    	}else{
    		cursorLoader = false;
    	}		
    	//getLoaderManager().getLoader(0).stopLoading();
    }
    
    @Override
    public void onLoaderReset(Loader<Cursor> arg0) {
    	mVideoListAdapter.swapCursor(null);	
    }

在onCreateLoader下查询并且返回CursorLoader,在onLoadFinished中,把Cursor加载进adapter中,这一套逻辑看起来并没有任何问题.而且我很早以前也写过图片管理器,和这个的原理逻辑基本上一样.

3. 这个时候我已经怀疑不是RK全家桶的锅了,进adb看的话,能看到挂载路径.

1
/mnt/media_rw/BC06-A913

在这里看到U盘的全部文件,而且find一下U盘的文件,能发现以下路径下也能找到,证明驱动那边应该是没问题的,已经挂载上了,锅可能就在framework里了

1
/storage/BC06-A913/

讲道理这个U盘也已经挂上去了,可播放器还是一样读不到,而CursorLoader其实就设置的查询条件去查数据库的数据,最终是调用ContentProvider的query方法进行查询,因此怀疑是数据库没有更新(这个坑我踩过),因此我发送广播进行强制刷新一下其中的一个视频文件.

1
2
3
4
5
6
7
8
9
10
11
12
	/**
	 * 通知媒体库更新文件
	 *
 	 * @param context
	 * @param filePath 文件全路径
	 */
	public static void scanFile(Context context, String filePath) {
		filePath = "/storage/BC06-A913/21.mp4";
		Intent scanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
		scanIntent.setData(Uri.fromFile(new File(filePath)));
		context.sendBroadcast(scanIntent);
	}

但是结果还是一样,播放器中找不到文件.

4. 去看看这个路径下的文件./packages/providers/MediaProvider因为播放器是用CursorLoader实现的,那么找providers下的MediaProvider来看看,如果锅在framework的话,这里的可能性就很大了,在/src/com/android/providers/media下的MediaScannerReceiver.java能搜索到对Intent.ACTION_MEDIA_SCANNER_SCAN_FILE的处理.

在对这个MediaScannerReceiver.java文件的分析,这个是一个Receiver,在onReceive方法里,可以看到一个很明显的rk的改动(我特意去看了AOSP的代码作为对比)

1
2
3
4
5
6
7
8
9
10
11
 if(("true".equals(SystemProperties.get("ro.udisk.visible")))){
	String id = intent.getStringExtra(VolumeInfo.EXTRA_VOLUME_ID);
	int state = intent.getIntExtra(VolumeInfo.EXTRA_VOLUME_STATE,-1);
	if(VolumeInfo.STATE_MOUNTED == state){
		Log.d(TAG,"----MediaScanner get volume mounted,start scan---  state : " + state);
		/*StorageManager mStorageManager = context.getSystemService(StorageManager.class);
		VolumeInfo vol = mStorageManager.findVolumeById(id);
		scanFile(context, vol.getPath().getPath());*/
		scan(context, MediaProvider.EXTERNAL_VOLUME);
	}
}

看到这里就基本上知道读不到U盘的原因了,必须要配置ro.udisk.visible=true

5. 为了验证结论,直接进入adb修改/system/build.prop文件,添加上ro.udisk.visible=true,再reboot. 然后完成重启后进入播放器就能读到U盘中的数据了.但是这个样只是临时修改,要新生产的固件也能使用的话,需要添加./device/rockchip/common/device.mk上添加,然后重新编译打包即可. (PS:这里感谢一下RK那边的老哥们的指点,和他们交流下也能学到不少.)

问题解决了,现在要研究一下原理的东西了

Android usb MediaProvider 原理分析

关键的东西还是在./packages/providers/MediaProvider下.

1
2
public class MediaScannerReceiver extends BroadcastReceiver {}
public class MediaScannerService extends Service implements Runnable{}

1. MediaScannerReceiver负责接受广播,接受一些U盘的路径之类的信息,在开机完成后,系统发了Intent.ACTION_BOOT_COMPLETED, 而MediaScannerReceiver过滤到这个action后就会扫描内部和外部储存.

1
2
3
4
5
6
7
8
9
10
11
12
@Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        final Uri uri = intent.getData();
        if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
            // Scan both internal and external storage
            scan(context, MediaProvider.INTERNAL_VOLUME);
            scan(context, MediaProvider.EXTERNAL_VOLUME);
            Log.d(TAG,action);
        }
        //....还有一部分,分开解释
   } 

2. 这里rk添加了一个配置其实为了可以在配置文件上修改,可以简单开关此功能,原理是这里拦截的action是指卷的状态改变,就是U盘的插拔..

1
2
3
4
5
6
7
8
9
10
11
12
if((VolumeInfo.ACTION_VOLUME_STATE_CHANGED.equals(action))){
	if(("true".equals(SystemProperties.get("ro.udisk.visible")))){
		String id = intent.getStringExtra(VolumeInfo.EXTRA_VOLUME_ID);
		int state = intent.getIntExtra(VolumeInfo.EXTRA_VOLUME_STATE,-1);
		if(VolumeInfo.STATE_MOUNTED == state){
			Log.d(TAG,"----MediaScanner get volume mounted,start scan---  state : " + state);
			/*StorageManager mStorageManager = context.getSystemService(StorageManager.class);
			VolumeInfo vol = mStorageManager.findVolumeById(id);
			scanFile(context, vol.getPath().getPath());*/
			scan(context, MediaProvider.EXTERNAL_VOLUME);
		}
	 }

3. 这里就是我上文说的,接受发送过来的信息来刷新数据库,可以看到使用了同一个action,Intent.ACTION_MEDIA_SCANNER_SCAN_FILE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
if (uri != null && uri.getScheme() != null && uri.getScheme().equals("file")) {
                // handle intents related to external storage
                String path = uri.getPath();
                String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
                String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath();

                try {
                    path = new File(path).getCanonicalPath();
                    Log.d(TAG,"File(path).getCanonicalPath() : "+path);
                } catch (IOException e) {
                    Log.e(TAG, "couldn't canonicalize " + path);
                    return;
                }
                if (path.startsWith(legacyPath)) {
                    path = externalStoragePath + path.substring(legacyPath.length());
                    Log.d(TAG,"path.startsWith : "+path);
                }
    
                String packageName = intent.getStringExtra("package");
                Log.d(TAG, "action: " + action + " path: " + path + " externalStoragePath:"+externalStoragePath);
                if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
                    // scan whenever any volume is mounted
                    scan(context, MediaProvider.EXTERNAL_VOLUME);
                } else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
                        path != null && path.startsWith(externalStoragePath + "/")) {
                    scanFile(context, path);
                } else if ( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
                        "RockExplorer".equals(packageName)) {
                    scan(context, MediaProvider.INTERNAL_VOLUME);
                    scan(context, MediaProvider.EXTERNAL_VOLUME);
                }
            }

4. 而下面这个两个方法才是去启动扫描文件,上面那些都是接受信息和处理逻辑,又看到这两个方法其实是启动了一个Service,所以真正扫描处理是在MediaScannerService.java中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 private void scan(Context context, String volume) {
        Log.d(TAG, "volume: " + volume);
        Bundle args = new Bundle();
        args.putString("volume", volume);
        context.startService(
                new Intent(context, MediaScannerService.class).putExtras(args));
    } 

    private void scanFile(Context context, String path) {
        Log.d(TAG, "filepath: " + path);
        Bundle args = new Bundle();
        args.putString("filepath", path);
        context.startService(
                new Intent(context, MediaScannerService.class).putExtras(args));
    }

5. MediaScannerService也就一个Service,按照生命周期去看,就做了一些初始化,注意这里开了新的线程,毕竟扫描是耗时的,而Service其实是在main中的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @Override
    public void onCreate()
    {
        PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
        StorageManager storageManager = (StorageManager)getSystemService(Context.STORAGE_SERVICE);
        mExternalStoragePaths = storageManager.getVolumePaths();

        // Start up the thread running the service.  Note that we create a
        // separate thread because the service normally runs in the process's
        // main thread, which we don't want to block.
        Thread thr = new Thread(null, this, "MediaScannerService");
        thr.start();
    }

6. 到onStartCommand下的写法我觉得这就很骚了…一直在等待mServiceHandler的完成new(这里我也特意去对比AOSP的代码,原生的也是这样写…),而且在判断intent是否为空,空的话返回Service.START_NOT_STICKY,这样这个Service被kill后就不会restart了,执行到最后返回Service.START_REDELIVER_INTENT,如果Service被kill了,那么系统会把之前的Intent再次发送给Service,直到Service完成处理.(细节处理好评)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    @Override
    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();
        Log.d(TAG, "msg.arg1 : "+ startId+" msg.obj : " + msg.obj);
        mServiceHandler.sendMessage(msg);
    
        // Try again later if we are killed before we can finish scanning.
        return Service.START_REDELIVER_INTENT;
    }

7. 看到这里就直到上文的mServiceHandler一直在等待new吧,毕竟不能确定是先执行到onStartCommand,还是在线程中的mServiceHandler先new,看到这里就能发现,处理扫描的任务应该都是在这个ServiceHandler中了.

1
2
3
4
5
6
7
8
9
10
11
12
public void run(){
        // reduce priority below other background threads to avoid interfering
        // with other services at boot time.
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
                Process.THREAD_PRIORITY_LESS_FAVORABLE);
        Looper.prepare();

        mServiceLooper = Looper.myLooper();
        mServiceHandler = new ServiceHandler();
    
        Looper.loop();
}

8. 在ServiceHandler中就是一堆逻辑处理后调用了scanFilescan两个方法,上文就说了CursorLoader最后也是调用ContentProvider的query方法进行查询,那ContentProvider的数据哪里来?就是这个在scanFile里调用openDatabase了然后getContentResolver().insert()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private Uri scanFile(String path, String mimeType) {
        String volumeName = MediaProvider.EXTERNAL_VOLUME;
        openDatabase(volumeName);
        MediaScanner scanner = createMediaScanner();
        try {
            // make sure the file path is in canonical form
            String canonicalPath = new File(path).getCanonicalPath();
            return scanner.scanSingleFile(canonicalPath, volumeName, mimeType);
        } catch (Exception e) {
            Log.e(TAG, "bad path " + path + " in scanFile()", e);
            return null;
        }
    }
    
   private void openDatabase(String volumeName) {
        try {
            ContentValues values = new ContentValues();
            values.put("name", volumeName);
            getContentResolver().insert(Uri.parse("content://media/"), values);
        } catch (IllegalArgumentException ex) {
            Log.w(TAG, "failed to open media database");
        }         
    } 

在scan下用WakeLock.acquire()锁住不让进入休眠,告知系统开始扫描这个路径下的文件,getContentResolver().insert插入数据,完成扫描再告诉系统扫描完成,mWakeLock.release()释放WakeLock,可以进入休眠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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);
                }
    
                MediaScanner scanner = createMediaScanner();
                scanner.scanDirectories(directories, volumeName);
            } 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();
        }
    }

总结

原理 : 插入U盘后MediaScannerReceiver接受到系统发出的action后,启动MediaScannerService去进行扫描文件,把数据插入到ContentProvider,通过ContentResolver来进行操作,然后在播放器中用CursorLoader来读取.

This post is licensed under CC BY 4.0 by the author.