1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
|
/*
* 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();
}
}
}
|