blob: 6ed4ea1db4994aee4f0a9adaf934aaf4340677bd [file] [log] [blame]
Daichi Hirono6baa16e2015-08-12 13:51:59 +09001/*
2 * Copyright (C) 2015 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
17package com.android.mtp;
18
Daichi Hirono4e94b8d2016-02-21 22:42:41 +090019import android.annotation.Nullable;
20import android.annotation.WorkerThread;
Daichi Hirono6baa16e2015-08-12 13:51:59 +090021import android.content.ContentResolver;
22import android.database.Cursor;
Daichi Hirono64111e02016-03-24 21:07:38 +090023import android.mtp.MtpConstants;
Tomasz Mikolajewskibb430fa2015-08-25 18:34:30 +090024import android.mtp.MtpObjectInfo;
Daichi Hirono6baa16e2015-08-12 13:51:59 +090025import android.net.Uri;
26import android.os.Bundle;
27import android.os.Process;
28import android.provider.DocumentsContract;
Daichi Hironocfaab202016-02-05 16:04:19 +090029import android.util.Log;
Daichi Hirono6baa16e2015-08-12 13:51:59 +090030
Daichi Hirono4e94b8d2016-02-21 22:42:41 +090031import com.android.internal.util.Preconditions;
32
Daichi Hirono47eb1922015-11-16 13:01:31 +090033import java.io.FileNotFoundException;
Daichi Hirono6baa16e2015-08-12 13:51:59 +090034import java.io.IOException;
Daichi Hironocfaab202016-02-05 16:04:19 +090035import java.util.ArrayList;
Daichi Hirono6baa16e2015-08-12 13:51:59 +090036import java.util.Date;
37import java.util.LinkedList;
38
Daichi Hironod40b0302015-08-17 16:10:05 +090039/**
40 * Loader for MTP document.
41 * At the first request, the loader returns only first NUM_INITIAL_ENTRIES. Then it launches
42 * background thread to load the rest documents and caches its result for next requests.
Tomasz Mikolajewskibb430fa2015-08-25 18:34:30 +090043 * TODO: Rename this class to ObjectInfoLoader
Daichi Hironod40b0302015-08-17 16:10:05 +090044 */
Daichi Hirono4e94b8d2016-02-21 22:42:41 +090045class DocumentLoader implements AutoCloseable {
Daichi Hirono6baa16e2015-08-12 13:51:59 +090046 static final int NUM_INITIAL_ENTRIES = 10;
47 static final int NUM_LOADING_ENTRIES = 20;
48 static final int NOTIFY_PERIOD_MS = 500;
49
Daichi Hirono61ba9232016-02-26 12:58:39 +090050 private final MtpDeviceRecord mDevice;
Daichi Hirono6baa16e2015-08-12 13:51:59 +090051 private final MtpManager mMtpManager;
52 private final ContentResolver mResolver;
Daichi Hirono47eb1922015-11-16 13:01:31 +090053 private final MtpDatabase mDatabase;
Daichi Hironod40b0302015-08-17 16:10:05 +090054 private final TaskList mTaskList = new TaskList();
Daichi Hirono4e94b8d2016-02-21 22:42:41 +090055 private Thread mBackgroundThread;
Daichi Hirono6baa16e2015-08-12 13:51:59 +090056
Daichi Hirono61ba9232016-02-26 12:58:39 +090057 DocumentLoader(MtpDeviceRecord device, MtpManager mtpManager, ContentResolver resolver,
Daichi Hirono4e94b8d2016-02-21 22:42:41 +090058 MtpDatabase database) {
Daichi Hirono61ba9232016-02-26 12:58:39 +090059 mDevice = device;
Daichi Hirono6baa16e2015-08-12 13:51:59 +090060 mMtpManager = mtpManager;
61 mResolver = resolver;
Daichi Hirono47eb1922015-11-16 13:01:31 +090062 mDatabase = database;
Daichi Hirono6baa16e2015-08-12 13:51:59 +090063 }
64
Daichi Hirono4e94b8d2016-02-21 22:42:41 +090065 /**
66 * Queries the child documents of given parent.
67 * It loads the first NUM_INITIAL_ENTRIES of object info, then launches the background thread
68 * to load the rest.
69 */
70 synchronized Cursor queryChildDocuments(String[] columnNames, Identifier parent)
71 throws IOException {
Daichi Hirono071313e2016-03-18 17:34:29 +090072 assert parent.mDeviceId == mDevice.deviceId;
73
Daichi Hirono4e94b8d2016-02-21 22:42:41 +090074 LoaderTask task = mTaskList.findTask(parent);
75 if (task == null) {
76 if (parent.mDocumentId == null) {
77 throw new FileNotFoundException("Parent not found.");
78 }
79 // TODO: Handle nit race around here.
80 // 1. getObjectHandles.
81 // 2. putNewDocument.
82 // 3. startAddingChildDocuemnts.
83 // 4. stopAddingChildDocuments - It removes the new document added at the step 2,
84 // because it is not updated between start/stopAddingChildDocuments.
Daichi Hirono678ed362016-03-18 15:05:53 +090085 task = new LoaderTask(mMtpManager, mDatabase, mDevice.operationsSupported, parent);
86 task.loadObjectHandles();
87 task.loadObjectInfoList(NUM_INITIAL_ENTRIES);
Daichi Hirono4e94b8d2016-02-21 22:42:41 +090088 } else {
89 // Once remove the existing task in order to add it to the head of the list.
90 mTaskList.remove(task);
91 }
92
93 mTaskList.addFirst(task);
94 if (task.getState() == LoaderTask.STATE_LOADING) {
95 resume();
96 }
97 return task.createCursor(mResolver, columnNames);
98 }
99
100 /**
101 * Resumes a background thread.
102 */
103 synchronized void resume() {
104 if (mBackgroundThread == null) {
105 mBackgroundThread = new BackgroundLoaderThread();
106 mBackgroundThread.start();
107 }
108 }
109
110 /**
111 * Obtains next task to be run in background thread, or release the reference to background
112 * thread.
113 *
114 * Worker thread that receives null task needs to exit.
115 */
116 @WorkerThread
117 synchronized @Nullable LoaderTask getNextTaskOrReleaseBackgroundThread() {
118 Preconditions.checkState(mBackgroundThread != null);
119
Daichi Hirono76be46f2016-04-08 09:48:02 +0900120 for (final LoaderTask task : mTaskList) {
121 if (task.getState() == LoaderTask.STATE_LOADING) {
122 return task;
123 }
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900124 }
125
Daichi Hirono61ba9232016-02-26 12:58:39 +0900126 final Identifier identifier = mDatabase.getUnmappedDocumentsParent(mDevice.deviceId);
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900127 if (identifier != null) {
128 final LoaderTask existingTask = mTaskList.findTask(identifier);
129 if (existingTask != null) {
130 Preconditions.checkState(existingTask.getState() != LoaderTask.STATE_LOADING);
131 mTaskList.remove(existingTask);
132 }
Daichi Hirono678ed362016-03-18 15:05:53 +0900133 final LoaderTask newTask = new LoaderTask(
134 mMtpManager, mDatabase, mDevice.operationsSupported, identifier);
135 newTask.loadObjectHandles();
136 mTaskList.addFirst(newTask);
137 return newTask;
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900138 }
139
140 mBackgroundThread = null;
141 return null;
142 }
143
144 /**
145 * Terminates background thread.
146 */
147 @Override
148 public void close() throws InterruptedException {
149 final Thread thread;
150 synchronized (this) {
151 mTaskList.clear();
152 thread = mBackgroundThread;
153 }
154 if (thread != null) {
155 thread.interrupt();
156 thread.join();
157 }
158 }
159
160 synchronized void clearCompletedTasks() {
161 mTaskList.clearCompletedTasks();
162 }
163
Daichi Hirono76be46f2016-04-08 09:48:02 +0900164 /**
165 * Cancels the task for |parentIdentifier|.
166 *
167 * Task is removed from the cached list and it will create new task when |parentIdentifier|'s
168 * children are queried next.
169 */
170 void cancelTask(Identifier parentIdentifier) {
171 final LoaderTask task;
172 synchronized (this) {
173 task = mTaskList.findTask(parentIdentifier);
174 }
175 if (task != null) {
176 task.cancel();
177 mTaskList.remove(task);
178 }
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900179 }
180
181 /**
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900182 * Background thread to fetch object info.
183 */
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900184 private class BackgroundLoaderThread extends Thread {
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900185 /**
186 * Finds task that needs to be processed, then loads NUM_LOADING_ENTRIES of object info and
187 * store them to the database. If it does not find a task, exits the thread.
188 */
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900189 @Override
190 public void run() {
191 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900192 while (!Thread.interrupted()) {
193 final LoaderTask task = getNextTaskOrReleaseBackgroundThread();
194 if (task == null) {
195 return;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900196 }
Daichi Hirono678ed362016-03-18 15:05:53 +0900197 task.loadObjectInfoList(NUM_LOADING_ENTRIES);
198 final boolean shouldNotify =
Daichi Hirono3edcde22016-04-12 11:29:07 +0900199 task.getState() != LoaderTask.STATE_CANCELLED &&
200 (task.mLastNotified.getTime() <
201 new Date().getTime() - NOTIFY_PERIOD_MS ||
202 task.getState() != LoaderTask.STATE_LOADING);
Daichi Hirono678ed362016-03-18 15:05:53 +0900203 if (shouldNotify) {
204 task.notify(mResolver);
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900205 }
206 }
207 }
208 }
209
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900210 /**
211 * Task list that has helper methods to search/clear tasks.
212 */
Daichi Hironod40b0302015-08-17 16:10:05 +0900213 private static class TaskList extends LinkedList<LoaderTask> {
214 LoaderTask findTask(Identifier parent) {
215 for (int i = 0; i < size(); i++) {
216 if (get(i).mIdentifier.equals(parent))
217 return get(i);
218 }
219 return null;
220 }
221
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900222 void clearCompletedTasks() {
Daichi Hironod40b0302015-08-17 16:10:05 +0900223 int i = 0;
224 while (i < size()) {
Daichi Hirono47eb1922015-11-16 13:01:31 +0900225 if (get(i).getState() == LoaderTask.STATE_COMPLETED) {
Daichi Hironod40b0302015-08-17 16:10:05 +0900226 remove(i);
227 } else {
228 i++;
229 }
230 }
231 }
232 }
233
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900234 /**
235 * Loader task.
236 * Each task is responsible for fetching child documents for the given parent document.
237 */
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900238 private static class LoaderTask {
Daichi Hirono678ed362016-03-18 15:05:53 +0900239 static final int STATE_START = 0;
240 static final int STATE_LOADING = 1;
241 static final int STATE_COMPLETED = 2;
242 static final int STATE_ERROR = 3;
Daichi Hirono76be46f2016-04-08 09:48:02 +0900243 static final int STATE_CANCELLED = 4;
Daichi Hirono47eb1922015-11-16 13:01:31 +0900244
Daichi Hirono678ed362016-03-18 15:05:53 +0900245 final MtpManager mManager;
Daichi Hirono47eb1922015-11-16 13:01:31 +0900246 final MtpDatabase mDatabase;
Daichi Hirono37a655a2016-03-04 18:43:21 +0900247 final int[] mOperationsSupported;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900248 final Identifier mIdentifier;
Daichi Hirono678ed362016-03-18 15:05:53 +0900249 int[] mObjectHandles;
250 int mState;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900251 Date mLastNotified;
Daichi Hirono678ed362016-03-18 15:05:53 +0900252 int mPosition;
253 IOException mError;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900254
Daichi Hirono678ed362016-03-18 15:05:53 +0900255 LoaderTask(MtpManager manager, MtpDatabase database, int[] operationsSupported,
256 Identifier identifier) {
Daichi Hirono071313e2016-03-18 17:34:29 +0900257 assert operationsSupported != null;
258 assert identifier.mDocumentType != MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE;
Daichi Hirono678ed362016-03-18 15:05:53 +0900259 mManager = manager;
Daichi Hirono47eb1922015-11-16 13:01:31 +0900260 mDatabase = database;
Daichi Hirono61ba9232016-02-26 12:58:39 +0900261 mOperationsSupported = operationsSupported;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900262 mIdentifier = identifier;
Daichi Hirono678ed362016-03-18 15:05:53 +0900263 mObjectHandles = null;
264 mState = STATE_START;
265 mPosition = 0;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900266 mLastNotified = new Date();
267 }
268
Daichi Hirono678ed362016-03-18 15:05:53 +0900269 synchronized void loadObjectHandles() {
270 assert mState == STATE_START;
Daichi Hirono76be46f2016-04-08 09:48:02 +0900271 mPosition = 0;
Daichi Hirono678ed362016-03-18 15:05:53 +0900272 int parentHandle = mIdentifier.mObjectHandle;
273 // Need to pass the special value MtpManager.OBJECT_HANDLE_ROOT_CHILDREN to
274 // getObjectHandles if we would like to obtain children under the root.
275 if (mIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) {
276 parentHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN;
277 }
278 try {
279 mObjectHandles = mManager.getObjectHandles(
280 mIdentifier.mDeviceId, mIdentifier.mStorageId, parentHandle);
281 mState = STATE_LOADING;
282 } catch (IOException error) {
283 mError = error;
284 mState = STATE_ERROR;
285 }
286 }
287
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900288 /**
289 * Returns a cursor that traverses the child document of the parent document handled by the
290 * task.
291 * The returned task may have a EXTRA_LOADING flag.
292 */
Daichi Hirono678ed362016-03-18 15:05:53 +0900293 synchronized Cursor createCursor(ContentResolver resolver, String[] columnNames)
294 throws IOException {
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900295 final Bundle extras = new Bundle();
Daichi Hirono47eb1922015-11-16 13:01:31 +0900296 switch (getState()) {
297 case STATE_LOADING:
298 extras.putBoolean(DocumentsContract.EXTRA_LOADING, true);
299 break;
300 case STATE_ERROR:
Daichi Hirono678ed362016-03-18 15:05:53 +0900301 throw mError;
Daichi Hirono47eb1922015-11-16 13:01:31 +0900302 }
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900303 final Cursor cursor =
304 mDatabase.queryChildDocuments(columnNames, mIdentifier.mDocumentId);
Daichi Hirono76be46f2016-04-08 09:48:02 +0900305 cursor.setExtras(extras);
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900306 cursor.setNotificationUri(resolver, createUri());
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900307 return cursor;
308 }
309
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900310 /**
Daichi Hirono678ed362016-03-18 15:05:53 +0900311 * Stores object information into database.
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900312 */
Daichi Hirono678ed362016-03-18 15:05:53 +0900313 void loadObjectInfoList(int count) {
314 synchronized (this) {
315 if (mState != STATE_LOADING) {
316 return;
317 }
318 if (mPosition == 0) {
319 try{
320 mDatabase.getMapper().startAddingDocuments(mIdentifier.mDocumentId);
321 } catch (FileNotFoundException error) {
322 mError = error;
323 mState = STATE_ERROR;
324 return;
325 }
326 }
327 }
328 final ArrayList<MtpObjectInfo> infoList = new ArrayList<>();
329 for (int chunkEnd = mPosition + count;
330 mPosition < mObjectHandles.length && mPosition < chunkEnd;
331 mPosition++) {
332 try {
333 infoList.add(mManager.getObjectInfo(
334 mIdentifier.mDeviceId, mObjectHandles[mPosition]));
335 } catch (IOException error) {
336 Log.e(MtpDocumentsProvider.TAG, "Failed to load object info", error);
337 }
338 }
Daichi Hirono64111e02016-03-24 21:07:38 +0900339 final long[] objectSizeList = new long[infoList.size()];
340 for (int i = 0; i < infoList.size(); i++) {
341 final MtpObjectInfo info = infoList.get(i);
342 // Compressed size is 32-bit unsigned integer but getCompressedSize returns the
343 // value in Java int (signed 32-bit integer). Use getCompressedSizeLong instead
344 // to get the value in Java long.
345 if (info.getCompressedSizeLong() != 0xffffffffl) {
346 objectSizeList[i] = info.getCompressedSizeLong();
347 continue;
348 }
349
350 if (!MtpDeviceRecord.isSupported(
351 mOperationsSupported,
352 MtpConstants.OPERATION_GET_OBJECT_PROP_DESC) ||
353 !MtpDeviceRecord.isSupported(
354 mOperationsSupported,
355 MtpConstants.OPERATION_GET_OBJECT_PROP_VALUE)) {
356 objectSizeList[i] = -1;
357 continue;
358 }
359
360 // Object size is more than 4GB.
361 try {
362 objectSizeList[i] = mManager.getObjectSizeLong(
363 mIdentifier.mDeviceId,
364 info.getObjectHandle(),
365 info.getFormat());
366 } catch (IOException error) {
367 Log.e(MtpDocumentsProvider.TAG, "Failed to get object size property.", error);
368 objectSizeList[i] = -1;
369 }
370 }
Daichi Hirono678ed362016-03-18 15:05:53 +0900371 synchronized (this) {
Daichi Hirono76be46f2016-04-08 09:48:02 +0900372 // Check if the task is cancelled or not.
373 if (mState != STATE_LOADING) {
374 return;
375 }
Daichi Hirono678ed362016-03-18 15:05:53 +0900376 try {
377 mDatabase.getMapper().putChildDocuments(
378 mIdentifier.mDeviceId,
379 mIdentifier.mDocumentId,
380 mOperationsSupported,
Daichi Hirono64111e02016-03-24 21:07:38 +0900381 infoList.toArray(new MtpObjectInfo[infoList.size()]),
382 objectSizeList);
Daichi Hirono678ed362016-03-18 15:05:53 +0900383 } catch (FileNotFoundException error) {
384 // Looks like the parent document information is removed.
385 // Adding documents has already cancelled in Mapper so we don't need to invoke
386 // stopAddingDocuments.
387 mError = error;
388 mState = STATE_ERROR;
389 return;
390 }
391 if (mPosition >= mObjectHandles.length) {
392 try{
393 mDatabase.getMapper().stopAddingDocuments(mIdentifier.mDocumentId);
394 mState = STATE_COMPLETED;
395 } catch (FileNotFoundException error) {
396 mError = error;
397 mState = STATE_ERROR;
398 return;
399 }
400 }
Daichi Hirono47eb1922015-11-16 13:01:31 +0900401 }
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900402 }
403
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900404 /**
Daichi Hirono76be46f2016-04-08 09:48:02 +0900405 * Cancels the task.
406 */
407 synchronized void cancel() {
408 mDatabase.getMapper().cancelAddingDocuments(mIdentifier.mDocumentId);
409 mState = STATE_CANCELLED;
410 }
411
412 /**
Daichi Hirono678ed362016-03-18 15:05:53 +0900413 * Returns a state of the task.
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900414 */
Daichi Hirono678ed362016-03-18 15:05:53 +0900415 int getState() {
416 return mState;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900417 }
418
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900419 /**
420 * Notifies a change of child list of the document.
421 */
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900422 void notify(ContentResolver resolver) {
423 resolver.notifyChange(createUri(), null, false);
424 mLastNotified = new Date();
425 }
426
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900427 private Uri createUri() {
428 return DocumentsContract.buildChildDocumentsUri(
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900429 MtpDocumentsProvider.AUTHORITY, mIdentifier.mDocumentId);
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900430 }
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900431 }
432}