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.eventsSupported, 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