/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mtp; import android.content.ContentResolver; import android.net.Uri; import android.os.Process; import android.provider.DocumentsContract; import android.util.Log; import java.io.FileNotFoundException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; final class RootScanner { /** * Polling interval in milliseconds used for first SHORT_POLLING_TIMES because it is more * likely to add new root just after the device is added. */ private final static long SHORT_POLLING_INTERVAL = 2000; /** * Polling interval in milliseconds for low priority polling, when changes are not expected. */ private final static long LONG_POLLING_INTERVAL = 30 * 1000; /** * @see #SHORT_POLLING_INTERVAL */ private final static long SHORT_POLLING_TIMES = 10; /** * Milliseconds we wait for background thread when pausing. */ private final static long AWAIT_TERMINATION_TIMEOUT = 2000; final ContentResolver mResolver; final MtpManager mManager; final MtpDatabase mDatabase; ExecutorService mExecutor; private UpdateRootsRunnable mCurrentTask; RootScanner( ContentResolver resolver, MtpManager manager, MtpDatabase database) { mResolver = resolver; mManager = manager; mDatabase = database; } /** * Notifies a change of the roots list via ContentResolver. */ void notifyChange() { final Uri uri = DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY); mResolver.notifyChange(uri, null, false); } /** * Starts to check new changes right away. */ synchronized CountDownLatch resume() { if (mExecutor == null) { // Only single thread updates the database. mExecutor = Executors.newSingleThreadExecutor(); } if (mCurrentTask != null) { // Stop previous task. mCurrentTask.stop(); } mCurrentTask = new UpdateRootsRunnable(); mExecutor.execute(mCurrentTask); return mCurrentTask.mFirstScanCompleted; } /** * Stops background thread and wait for its termination. * @throws InterruptedException */ synchronized void pause() throws InterruptedException, TimeoutException { if (mExecutor == null) { return; } mExecutor.shutdownNow(); try { if (!mExecutor.awaitTermination(AWAIT_TERMINATION_TIMEOUT, TimeUnit.MILLISECONDS)) { throw new TimeoutException( "Timeout for terminating RootScanner's background thread."); } } finally { mExecutor = null; } } /** * Runnable to scan roots and update the database information. */ private final class UpdateRootsRunnable implements Runnable { /** * Count down latch that specifies the runnable is stopped. */ final CountDownLatch mStopped = new CountDownLatch(1); /** * Count down latch that specifies the first scan is completed. */ final CountDownLatch mFirstScanCompleted = new CountDownLatch(1); @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); int pollingCount = 0; while (mStopped.getCount() > 0) { boolean changed = false; // Update devices. final MtpDeviceRecord[] devices = mManager.getDevices(); try { mDatabase.getMapper().startAddingDocuments(null /* parentDocumentId */); for (final MtpDeviceRecord device : devices) { if (mDatabase.getMapper().putDeviceDocument(device)) { changed = true; } } if (mDatabase.getMapper().stopAddingDocuments( null /* parentDocumentId */)) { changed = true; } } catch (FileNotFoundException exception) { // The top root (ID is null) must exist always. // FileNotFoundException is unexpected. Log.e(MtpDocumentsProvider.TAG, "Unexpected FileNotFoundException", exception); throw new AssertionError("Unexpected exception for the top parent", exception); } // Update roots. for (final MtpDeviceRecord device : devices) { final String documentId = mDatabase.getDocumentIdForDevice(device.deviceId); if (documentId == null) { continue; } try { mDatabase.getMapper().startAddingDocuments(documentId); if (mDatabase.getMapper().putStorageDocuments( documentId, device.operationsSupported, device.roots)) { changed = true; } if (mDatabase.getMapper().stopAddingDocuments(documentId)) { changed = true; } } catch (FileNotFoundException exception) { Log.e(MtpDocumentsProvider.TAG, "Parent document is gone.", exception); continue; } } if (changed) { notifyChange(); } mFirstScanCompleted.countDown(); pollingCount++; if (devices.length == 0) { break; } try { // Use SHORT_POLLING_PERIOD for the first SHORT_POLLING_TIMES because it is // more likely to add new root just after the device is added. // TODO: Use short interval only for a device that is just added. mStopped.await(pollingCount > SHORT_POLLING_TIMES ? LONG_POLLING_INTERVAL : SHORT_POLLING_INTERVAL, TimeUnit.MILLISECONDS); } catch (InterruptedException exp) { break; } } } void stop() { mStopped.countDown(); } } }