blob: 329afdd4f17cda9237f509970043829490c95578 [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 =
199 task.mLastNotified.getTime() <
200 new Date().getTime() - NOTIFY_PERIOD_MS ||
201 task.getState() != LoaderTask.STATE_LOADING;
202 if (shouldNotify) {
203 task.notify(mResolver);
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900204 }
205 }
206 }
207 }
208
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900209 /**
210 * Task list that has helper methods to search/clear tasks.
211 */
Daichi Hironod40b0302015-08-17 16:10:05 +0900212 private static class TaskList extends LinkedList<LoaderTask> {
213 LoaderTask findTask(Identifier parent) {
214 for (int i = 0; i < size(); i++) {
215 if (get(i).mIdentifier.equals(parent))
216 return get(i);
217 }
218 return null;
219 }
220
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900221 void clearCompletedTasks() {
Daichi Hironod40b0302015-08-17 16:10:05 +0900222 int i = 0;
223 while (i < size()) {
Daichi Hirono47eb1922015-11-16 13:01:31 +0900224 if (get(i).getState() == LoaderTask.STATE_COMPLETED) {
Daichi Hironod40b0302015-08-17 16:10:05 +0900225 remove(i);
226 } else {
227 i++;
228 }
229 }
230 }
231 }
232
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900233 /**
234 * Loader task.
235 * Each task is responsible for fetching child documents for the given parent document.
236 */
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900237 private static class LoaderTask {
Daichi Hirono678ed362016-03-18 15:05:53 +0900238 static final int STATE_START = 0;
239 static final int STATE_LOADING = 1;
240 static final int STATE_COMPLETED = 2;
241 static final int STATE_ERROR = 3;
Daichi Hirono76be46f2016-04-08 09:48:02 +0900242 static final int STATE_CANCELLED = 4;
Daichi Hirono47eb1922015-11-16 13:01:31 +0900243
Daichi Hirono678ed362016-03-18 15:05:53 +0900244 final MtpManager mManager;
Daichi Hirono47eb1922015-11-16 13:01:31 +0900245 final MtpDatabase mDatabase;
Daichi Hirono37a655a2016-03-04 18:43:21 +0900246 final int[] mOperationsSupported;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900247 final Identifier mIdentifier;
Daichi Hirono678ed362016-03-18 15:05:53 +0900248 int[] mObjectHandles;
249 int mState;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900250 Date mLastNotified;
Daichi Hirono678ed362016-03-18 15:05:53 +0900251 int mPosition;
252 IOException mError;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900253
Daichi Hirono678ed362016-03-18 15:05:53 +0900254 LoaderTask(MtpManager manager, MtpDatabase database, int[] operationsSupported,
255 Identifier identifier) {
Daichi Hirono071313e2016-03-18 17:34:29 +0900256 assert operationsSupported != null;
257 assert identifier.mDocumentType != MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE;
Daichi Hirono678ed362016-03-18 15:05:53 +0900258 mManager = manager;
Daichi Hirono47eb1922015-11-16 13:01:31 +0900259 mDatabase = database;
Daichi Hirono61ba9232016-02-26 12:58:39 +0900260 mOperationsSupported = operationsSupported;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900261 mIdentifier = identifier;
Daichi Hirono678ed362016-03-18 15:05:53 +0900262 mObjectHandles = null;
263 mState = STATE_START;
264 mPosition = 0;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900265 mLastNotified = new Date();
266 }
267
Daichi Hirono678ed362016-03-18 15:05:53 +0900268 synchronized void loadObjectHandles() {
269 assert mState == STATE_START;
Daichi Hirono76be46f2016-04-08 09:48:02 +0900270 mPosition = 0;
Daichi Hirono678ed362016-03-18 15:05:53 +0900271 int parentHandle = mIdentifier.mObjectHandle;
272 // Need to pass the special value MtpManager.OBJECT_HANDLE_ROOT_CHILDREN to
273 // getObjectHandles if we would like to obtain children under the root.
274 if (mIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) {
275 parentHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN;
276 }
277 try {
278 mObjectHandles = mManager.getObjectHandles(
279 mIdentifier.mDeviceId, mIdentifier.mStorageId, parentHandle);
280 mState = STATE_LOADING;
281 } catch (IOException error) {
282 mError = error;
283 mState = STATE_ERROR;
284 }
285 }
286
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900287 /**
288 * Returns a cursor that traverses the child document of the parent document handled by the
289 * task.
290 * The returned task may have a EXTRA_LOADING flag.
291 */
Daichi Hirono678ed362016-03-18 15:05:53 +0900292 synchronized Cursor createCursor(ContentResolver resolver, String[] columnNames)
293 throws IOException {
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900294 final Bundle extras = new Bundle();
Daichi Hirono47eb1922015-11-16 13:01:31 +0900295 switch (getState()) {
296 case STATE_LOADING:
297 extras.putBoolean(DocumentsContract.EXTRA_LOADING, true);
298 break;
299 case STATE_ERROR:
Daichi Hirono678ed362016-03-18 15:05:53 +0900300 throw mError;
Daichi Hirono47eb1922015-11-16 13:01:31 +0900301 }
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900302 final Cursor cursor =
303 mDatabase.queryChildDocuments(columnNames, mIdentifier.mDocumentId);
Daichi Hirono76be46f2016-04-08 09:48:02 +0900304 cursor.setExtras(extras);
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900305 cursor.setNotificationUri(resolver, createUri());
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900306 return cursor;
307 }
308
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900309 /**
Daichi Hirono678ed362016-03-18 15:05:53 +0900310 * Stores object information into database.
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900311 */
Daichi Hirono678ed362016-03-18 15:05:53 +0900312 void loadObjectInfoList(int count) {
313 synchronized (this) {
314 if (mState != STATE_LOADING) {
315 return;
316 }
317 if (mPosition == 0) {
318 try{
319 mDatabase.getMapper().startAddingDocuments(mIdentifier.mDocumentId);
320 } catch (FileNotFoundException error) {
321 mError = error;
322 mState = STATE_ERROR;
323 return;
324 }
325 }
326 }
327 final ArrayList<MtpObjectInfo> infoList = new ArrayList<>();
328 for (int chunkEnd = mPosition + count;
329 mPosition < mObjectHandles.length && mPosition < chunkEnd;
330 mPosition++) {
331 try {
332 infoList.add(mManager.getObjectInfo(
333 mIdentifier.mDeviceId, mObjectHandles[mPosition]));
334 } catch (IOException error) {
335 Log.e(MtpDocumentsProvider.TAG, "Failed to load object info", error);
336 }
337 }
Daichi Hirono64111e02016-03-24 21:07:38 +0900338 final long[] objectSizeList = new long[infoList.size()];
339 for (int i = 0; i < infoList.size(); i++) {
340 final MtpObjectInfo info = infoList.get(i);
341 // Compressed size is 32-bit unsigned integer but getCompressedSize returns the
342 // value in Java int (signed 32-bit integer). Use getCompressedSizeLong instead
343 // to get the value in Java long.
344 if (info.getCompressedSizeLong() != 0xffffffffl) {
345 objectSizeList[i] = info.getCompressedSizeLong();
346 continue;
347 }
348
349 if (!MtpDeviceRecord.isSupported(
350 mOperationsSupported,
351 MtpConstants.OPERATION_GET_OBJECT_PROP_DESC) ||
352 !MtpDeviceRecord.isSupported(
353 mOperationsSupported,
354 MtpConstants.OPERATION_GET_OBJECT_PROP_VALUE)) {
355 objectSizeList[i] = -1;
356 continue;
357 }
358
359 // Object size is more than 4GB.
360 try {
361 objectSizeList[i] = mManager.getObjectSizeLong(
362 mIdentifier.mDeviceId,
363 info.getObjectHandle(),
364 info.getFormat());
365 } catch (IOException error) {
366 Log.e(MtpDocumentsProvider.TAG, "Failed to get object size property.", error);
367 objectSizeList[i] = -1;
368 }
369 }
Daichi Hirono678ed362016-03-18 15:05:53 +0900370 synchronized (this) {
Daichi Hirono76be46f2016-04-08 09:48:02 +0900371 // Check if the task is cancelled or not.
372 if (mState != STATE_LOADING) {
373 return;
374 }
Daichi Hirono678ed362016-03-18 15:05:53 +0900375 try {
376 mDatabase.getMapper().putChildDocuments(
377 mIdentifier.mDeviceId,
378 mIdentifier.mDocumentId,
379 mOperationsSupported,
Daichi Hirono64111e02016-03-24 21:07:38 +0900380 infoList.toArray(new MtpObjectInfo[infoList.size()]),
381 objectSizeList);
Daichi Hirono678ed362016-03-18 15:05:53 +0900382 } catch (FileNotFoundException error) {
383 // Looks like the parent document information is removed.
384 // Adding documents has already cancelled in Mapper so we don't need to invoke
385 // stopAddingDocuments.
386 mError = error;
387 mState = STATE_ERROR;
388 return;
389 }
390 if (mPosition >= mObjectHandles.length) {
391 try{
392 mDatabase.getMapper().stopAddingDocuments(mIdentifier.mDocumentId);
393 mState = STATE_COMPLETED;
394 } catch (FileNotFoundException error) {
395 mError = error;
396 mState = STATE_ERROR;
397 return;
398 }
399 }
Daichi Hirono47eb1922015-11-16 13:01:31 +0900400 }
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900401 }
402
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900403 /**
Daichi Hirono76be46f2016-04-08 09:48:02 +0900404 * Cancels the task.
405 */
406 synchronized void cancel() {
407 mDatabase.getMapper().cancelAddingDocuments(mIdentifier.mDocumentId);
408 mState = STATE_CANCELLED;
409 }
410
411 /**
Daichi Hirono678ed362016-03-18 15:05:53 +0900412 * Returns a state of the task.
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900413 */
Daichi Hirono678ed362016-03-18 15:05:53 +0900414 int getState() {
415 return mState;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900416 }
417
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900418 /**
419 * Notifies a change of child list of the document.
420 */
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900421 void notify(ContentResolver resolver) {
422 resolver.notifyChange(createUri(), null, false);
423 mLastNotified = new Date();
424 }
425
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900426 private Uri createUri() {
427 return DocumentsContract.buildChildDocumentsUri(
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900428 MtpDocumentsProvider.AUTHORITY, mIdentifier.mDocumentId);
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900429 }
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900430 }
431}