RootScanner.java revision 0f32537e40ee2662d4f0b7b625ee280ca9c02066
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 17package com.android.mtp; 18 19import android.content.ContentResolver; 20import android.net.Uri; 21import android.os.Process; 22import android.provider.DocumentsContract; 23import android.util.Log; 24 25import java.io.FileNotFoundException; 26import java.util.concurrent.CountDownLatch; 27import java.util.concurrent.ExecutorService; 28import java.util.concurrent.Executors; 29import java.util.concurrent.FutureTask; 30import java.util.concurrent.TimeUnit; 31 32final 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 FutureTask<Void> mCurrentTask; 60 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 */ 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 */ 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 // Cancel previous task. 88 mCurrentTask.cancel(true); 89 } 90 final UpdateRootsRunnable runnable = new UpdateRootsRunnable(); 91 mCurrentTask = new FutureTask<Void>(runnable, null); 92 mExecutor.submit(mCurrentTask); 93 return runnable.mFirstScanCompleted; 94 } 95 96 /** 97 * Stops background thread and wait for its termination. 98 * @throws InterruptedException 99 */ 100 synchronized void pause() throws InterruptedException { 101 if (mExecutor == null) { 102 return; 103 } 104 mExecutor.shutdownNow(); 105 if (!mExecutor.awaitTermination(AWAIT_TERMINATION_TIMEOUT, TimeUnit.MILLISECONDS)) { 106 Log.e(MtpDocumentsProvider.TAG, "Failed to terminate RootScanner's background thread."); 107 } 108 mExecutor = null; 109 } 110 111 /** 112 * Runnable to scan roots and update the database information. 113 */ 114 private final class UpdateRootsRunnable implements Runnable { 115 final CountDownLatch mFirstScanCompleted = new CountDownLatch(1); 116 117 @Override 118 public void run() { 119 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 120 int pollingCount = 0; 121 while (true) { 122 boolean changed = false; 123 124 // Update devices. 125 final MtpDeviceRecord[] devices = mManager.getDevices(); 126 try { 127 mDatabase.getMapper().startAddingDocuments(null /* parentDocumentId */); 128 for (final MtpDeviceRecord device : devices) { 129 if (mDatabase.getMapper().putDeviceDocument(device)) { 130 changed = true; 131 } 132 } 133 if (mDatabase.getMapper().stopAddingDocuments( 134 null /* parentDocumentId */)) { 135 changed = true; 136 } 137 } catch (FileNotFoundException exception) { 138 // The top root (ID is null) must exist always. 139 // FileNotFoundException is unexpected. 140 Log.e(MtpDocumentsProvider.TAG, "Unexpected FileNotFoundException", exception); 141 throw new AssertionError("Unexpected exception for the top parent", exception); 142 } 143 144 // Update roots. 145 for (final MtpDeviceRecord device : devices) { 146 final String documentId = mDatabase.getDocumentIdForDevice(device.deviceId); 147 if (documentId == null) { 148 continue; 149 } 150 try { 151 mDatabase.getMapper().startAddingDocuments(documentId); 152 if (mDatabase.getMapper().putStorageDocuments( 153 documentId, device.eventsSupported, device.roots)) { 154 changed = true; 155 } 156 if (mDatabase.getMapper().stopAddingDocuments(documentId)) { 157 changed = true; 158 } 159 } catch (FileNotFoundException exception) { 160 Log.e(MtpDocumentsProvider.TAG, "Parent document is gone.", exception); 161 continue; 162 } 163 } 164 165 if (changed) { 166 notifyChange(); 167 } 168 mFirstScanCompleted.countDown(); 169 pollingCount++; 170 try { 171 // Use SHORT_POLLING_PERIOD for the first SHORT_POLLING_TIMES because it is 172 // more likely to add new root just after the device is added. 173 // TODO: Use short interval only for a device that is just added. 174 Thread.sleep(pollingCount > SHORT_POLLING_TIMES ? 175 LONG_POLLING_INTERVAL : SHORT_POLLING_INTERVAL); 176 } catch (InterruptedException exp) { 177 break; 178 } 179 } 180 } 181 } 182} 183