/*
 * 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.
/** * 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.
/** * Polling interval in milliseconds for low priority polling, when changes are not expected. */
private final static long LONG_POLLING_INTERVAL = 30 * 1000;
See Also:
  • SHORT_POLLING_INTERVAL
/** * @see #SHORT_POLLING_INTERVAL */
private final static long SHORT_POLLING_TIMES = 10;
Milliseconds we wait for background thread when pausing.
/** * 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.
/** * 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.
/** * 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 –
/** * 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.
/** * Runnable to scan roots and update the database information. */
private final class UpdateRootsRunnable implements Runnable {
Count down latch that specifies the runnable is stopped.
/** * 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.
/** * 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(); } } }