Home | History | Annotate | Download | only in mtp
      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 
     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             // 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      */
     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
    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 
    196         void stop() {
    197             mStopped.countDown();
    198         }
    199     }
    200 }
    201