RootScanner.java revision b63e8c6ccc1425d56f8b9c801f4bddf906d694e5
1package com.android.mtp; 2 3import android.content.ContentResolver; 4import android.content.res.Resources; 5import android.database.sqlite.SQLiteException; 6import android.net.Uri; 7import android.os.Process; 8import android.provider.DocumentsContract; 9import android.util.Log; 10 11import java.io.IOException; 12import java.util.concurrent.ExecutorService; 13import java.util.concurrent.Executors; 14import java.util.concurrent.FutureTask; 15import java.util.concurrent.TimeUnit; 16 17final class RootScanner { 18 /** 19 * Polling interval in milliseconds used for first SHORT_POLLING_TIMES because it is more 20 * likely to add new root just after the device is added. 21 */ 22 private final static long SHORT_POLLING_INTERVAL = 2000; 23 24 /** 25 * Polling interval in milliseconds for low priority polling, when changes are not expected. 26 */ 27 private final static long LONG_POLLING_INTERVAL = 30 * 1000; 28 29 /** 30 * @see #SHORT_POLLING_INTERVAL 31 */ 32 private final static long SHORT_POLLING_TIMES = 10; 33 34 /** 35 * Milliseconds we wait for background thread when pausing. 36 */ 37 private final static long AWAIT_TERMINATION_TIMEOUT = 2000; 38 39 final ContentResolver mResolver; 40 final Resources mResources; 41 final MtpManager mManager; 42 final MtpDatabase mDatabase; 43 44 ExecutorService mExecutor; 45 FutureTask<Void> mCurrentTask; 46 47 RootScanner( 48 ContentResolver resolver, 49 Resources resources, 50 MtpManager manager, 51 MtpDatabase database) { 52 mResolver = resolver; 53 mResources = resources; 54 mManager = manager; 55 mDatabase = database; 56 } 57 58 /** 59 * Notifies a change of the roots list via ContentResolver. 60 */ 61 void notifyChange() { 62 final Uri uri = 63 DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY); 64 mResolver.notifyChange(uri, null, false); 65 } 66 67 /** 68 * Starts to check new changes right away. 69 * If the background thread has already gone, it restarts another background thread. 70 */ 71 synchronized void resume() { 72 if (mExecutor == null) { 73 // Only single thread updates the database. 74 mExecutor = Executors.newSingleThreadExecutor(); 75 } 76 if (mCurrentTask != null) { 77 // Cancel previous task. 78 mCurrentTask.cancel(true); 79 } 80 mCurrentTask = new FutureTask<Void>(new UpdateRootsRunnable(), null); 81 mExecutor.submit(mCurrentTask); 82 } 83 84 /** 85 * Stops background thread and wait for its termination. 86 * @throws InterruptedException 87 */ 88 synchronized void pause() throws InterruptedException { 89 if (mExecutor == null) { 90 return; 91 } 92 mExecutor.shutdownNow(); 93 if (!mExecutor.awaitTermination(AWAIT_TERMINATION_TIMEOUT, TimeUnit.MILLISECONDS)) { 94 Log.e(MtpDocumentsProvider.TAG, "Failed to terminate RootScanner's background thread."); 95 } 96 mExecutor = null; 97 } 98 99 /** 100 * Runnable to scan roots and update the database information. 101 */ 102 private final class UpdateRootsRunnable implements Runnable { 103 @Override 104 public void run() { 105 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 106 int pollingCount = 0; 107 while (!Thread.interrupted()) { 108 final int[] deviceIds = mManager.getOpenedDeviceIds(); 109 if (deviceIds.length == 0) { 110 return; 111 } 112 boolean changed = false; 113 mDatabase.getMapper().startAddingDocuments(null /* parentDocumentId */); 114 for (int deviceId : deviceIds) { 115 try { 116 final MtpRoot[] roots = mManager.getRoots(deviceId); 117 if (mDatabase.getMapper().putRootDocuments(deviceId, mResources, roots)) { 118 changed = true; 119 } 120 } catch (IOException | SQLiteException exception) { 121 // The error may happen on the device. We would like to continue getting 122 // roots for other devices. 123 Log.e(MtpDocumentsProvider.TAG, exception.getMessage()); 124 } 125 } 126 if (mDatabase.getMapper().stopAddingDocuments(null /* parentDocumentId */)) { 127 changed = true; 128 } 129 if (changed) { 130 notifyChange(); 131 } 132 pollingCount++; 133 try { 134 // Use SHORT_POLLING_PERIOD for the first SHORT_POLLING_TIMES because it is 135 // more likely to add new root just after the device is added. 136 // TODO: Use short interval only for a device that is just added. 137 Thread.sleep(pollingCount > SHORT_POLLING_TIMES ? 138 LONG_POLLING_INTERVAL : SHORT_POLLING_INTERVAL); 139 } catch (InterruptedException exp) { 140 // The while condition handles the interrupted flag. 141 continue; 142 } 143 } 144 } 145 } 146} 147