1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.mtp; 18 19 import android.content.ContentResolver; 20 import android.net.Uri; 21 import android.os.Process; 22 import android.provider.DocumentsContract; 23 import android.util.Log; 24 25 import java.io.FileNotFoundException; 26 import java.util.concurrent.CountDownLatch; 27 import java.util.concurrent.ExecutorService; 28 import java.util.concurrent.Executors; 29 import java.util.concurrent.TimeUnit; 30 import java.util.concurrent.TimeoutException; 31 32 final class RootScanner { 33 /** 34 * Polling interval in milliseconds used for first SHORT_POLLING_TIMES because it is more 35 * likely to add new root just after the device is added. 36 */ 37 private final static long SHORT_POLLING_INTERVAL = 2000; 38 39 /** 40 * Polling interval in milliseconds for low priority polling, when changes are not expected. 41 */ 42 private final static long LONG_POLLING_INTERVAL = 30 * 1000; 43 44 /** 45 * @see #SHORT_POLLING_INTERVAL 46 */ 47 private final static long SHORT_POLLING_TIMES = 10; 48 49 /** 50 * Milliseconds we wait for background thread when pausing. 51 */ 52 private final static long AWAIT_TERMINATION_TIMEOUT = 2000; 53 54 final ContentResolver mResolver; 55 final MtpManager mManager; 56 final MtpDatabase mDatabase; 57 58 ExecutorService mExecutor; 59 private UpdateRootsRunnable mCurrentTask; 60 RootScanner( ContentResolver resolver, MtpManager manager, MtpDatabase database)61 RootScanner( 62 ContentResolver resolver, 63 MtpManager manager, 64 MtpDatabase database) { 65 mResolver = resolver; 66 mManager = manager; 67 mDatabase = database; 68 } 69 70 /** 71 * Notifies a change of the roots list via ContentResolver. 72 */ notifyChange()73 void notifyChange() { 74 final Uri uri = DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY); 75 mResolver.notifyChange(uri, null, false); 76 } 77 78 /** 79 * Starts to check new changes right away. 80 */ resume()81 synchronized CountDownLatch resume() { 82 if (mExecutor == null) { 83 // Only single thread updates the database. 84 mExecutor = Executors.newSingleThreadExecutor(); 85 } 86 if (mCurrentTask != null) { 87 // Stop previous task. 88 mCurrentTask.stop(); 89 } 90 mCurrentTask = new UpdateRootsRunnable(); 91 mExecutor.execute(mCurrentTask); 92 return mCurrentTask.mFirstScanCompleted; 93 } 94 95 /** 96 * Stops background thread and wait for its termination. 97 * @throws InterruptedException 98 */ pause()99 synchronized void pause() throws InterruptedException, TimeoutException { 100 if (mExecutor == null) { 101 return; 102 } 103 mExecutor.shutdownNow(); 104 try { 105 if (!mExecutor.awaitTermination(AWAIT_TERMINATION_TIMEOUT, TimeUnit.MILLISECONDS)) { 106 throw new TimeoutException( 107 "Timeout for terminating RootScanner's background thread."); 108 } 109 } finally { 110 mExecutor = null; 111 } 112 } 113 114 /** 115 * Runnable to scan roots and update the database information. 116 */ 117 private final class UpdateRootsRunnable implements Runnable { 118 /** 119 * Count down latch that specifies the runnable is stopped. 120 */ 121 final CountDownLatch mStopped = new CountDownLatch(1); 122 123 /** 124 * Count down latch that specifies the first scan is completed. 125 */ 126 final CountDownLatch mFirstScanCompleted = new CountDownLatch(1); 127 128 @Override run()129 public void run() { 130 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 131 int pollingCount = 0; 132 while (mStopped.getCount() > 0) { 133 boolean changed = false; 134 135 // Update devices. 136 final MtpDeviceRecord[] devices = mManager.getDevices(); 137 try { 138 mDatabase.getMapper().startAddingDocuments(null /* parentDocumentId */); 139 for (final MtpDeviceRecord device : devices) { 140 if (mDatabase.getMapper().putDeviceDocument(device)) { 141 changed = true; 142 } 143 } 144 if (mDatabase.getMapper().stopAddingDocuments( 145 null /* parentDocumentId */)) { 146 changed = true; 147 } 148 } catch (FileNotFoundException exception) { 149 // The top root (ID is null) must exist always. 150 // FileNotFoundException is unexpected. 151 Log.e(MtpDocumentsProvider.TAG, "Unexpected FileNotFoundException", exception); 152 throw new AssertionError("Unexpected exception for the top parent", exception); 153 } 154 155 // Update roots. 156 for (final MtpDeviceRecord device : devices) { 157 final String documentId = mDatabase.getDocumentIdForDevice(device.deviceId); 158 if (documentId == null) { 159 continue; 160 } 161 try { 162 mDatabase.getMapper().startAddingDocuments(documentId); 163 if (mDatabase.getMapper().putStorageDocuments( 164 documentId, device.operationsSupported, device.roots)) { 165 changed = true; 166 } 167 if (mDatabase.getMapper().stopAddingDocuments(documentId)) { 168 changed = true; 169 } 170 } catch (FileNotFoundException exception) { 171 Log.e(MtpDocumentsProvider.TAG, "Parent document is gone.", exception); 172 continue; 173 } 174 } 175 176 if (changed) { 177 notifyChange(); 178 } 179 mFirstScanCompleted.countDown(); 180 pollingCount++; 181 if (devices.length == 0) { 182 break; 183 } 184 try { 185 // Use SHORT_POLLING_PERIOD for the first SHORT_POLLING_TIMES because it is 186 // more likely to add new root just after the device is added. 187 // TODO: Use short interval only for a device that is just added. 188 mStopped.await(pollingCount > SHORT_POLLING_TIMES ? 189 LONG_POLLING_INTERVAL : SHORT_POLLING_INTERVAL, TimeUnit.MILLISECONDS); 190 } catch (InterruptedException exp) { 191 break; 192 } 193 } 194 } 195 stop()196 void stop() { 197 mStopped.countDown(); 198 } 199 } 200 } 201