1/*
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
19import android.annotation.Nullable;
20import android.annotation.WorkerThread;
21import android.content.ContentResolver;
22import android.database.Cursor;
23import android.mtp.MtpConstants;
24import android.mtp.MtpObjectInfo;
25import android.net.Uri;
26import android.os.Bundle;
27import android.os.Process;
28import android.provider.DocumentsContract;
29import android.util.Log;
30
31import com.android.internal.util.Preconditions;
32
33import java.io.FileNotFoundException;
34import java.io.IOException;
35import java.util.ArrayList;
36import java.util.Date;
37import java.util.LinkedList;
38
39/**
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.
43 * TODO: Rename this class to ObjectInfoLoader
44 */
45class DocumentLoader implements AutoCloseable {
46    static final int NUM_INITIAL_ENTRIES = 10;
47    static final int NUM_LOADING_ENTRIES = 20;
48    static final int NOTIFY_PERIOD_MS = 500;
49
50    private final MtpDeviceRecord mDevice;
51    private final MtpManager mMtpManager;
52    private final ContentResolver mResolver;
53    private final MtpDatabase mDatabase;
54    private final TaskList mTaskList = new TaskList();
55    private Thread mBackgroundThread;
56
57    DocumentLoader(MtpDeviceRecord device, MtpManager mtpManager, ContentResolver resolver,
58                   MtpDatabase database) {
59        mDevice = device;
60        mMtpManager = mtpManager;
61        mResolver = resolver;
62        mDatabase = database;
63    }
64
65    /**
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 {
72        assert parent.mDeviceId == mDevice.deviceId;
73
74        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.
85            task = new LoaderTask(mMtpManager, mDatabase, mDevice.operationsSupported, parent);
86            task.loadObjectHandles();
87            task.loadObjectInfoList(NUM_INITIAL_ENTRIES);
88        } 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
120        for (final LoaderTask task : mTaskList) {
121            if (task.getState() == LoaderTask.STATE_LOADING) {
122                return task;
123            }
124        }
125
126        final Identifier identifier = mDatabase.getUnmappedDocumentsParent(mDevice.deviceId);
127        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            }
133            final LoaderTask newTask = new LoaderTask(
134                    mMtpManager, mDatabase, mDevice.operationsSupported, identifier);
135            newTask.loadObjectHandles();
136            mTaskList.addFirst(newTask);
137            return newTask;
138        }
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
164    /**
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        }
179    }
180
181    /**
182     * Background thread to fetch object info.
183     */
184    private class BackgroundLoaderThread extends Thread {
185        /**
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         */
189        @Override
190        public void run() {
191            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
192            while (!Thread.interrupted()) {
193                final LoaderTask task = getNextTaskOrReleaseBackgroundThread();
194                if (task == null) {
195                    return;
196                }
197                task.loadObjectInfoList(NUM_LOADING_ENTRIES);
198                final boolean shouldNotify =
199                        task.getState() != LoaderTask.STATE_CANCELLED &&
200                        (task.mLastNotified.getTime() <
201                         new Date().getTime() - NOTIFY_PERIOD_MS ||
202                         task.getState() != LoaderTask.STATE_LOADING);
203                if (shouldNotify) {
204                    task.notify(mResolver);
205                }
206            }
207        }
208    }
209
210    /**
211     * Task list that has helper methods to search/clear tasks.
212     */
213    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
222        void clearCompletedTasks() {
223            int i = 0;
224            while (i < size()) {
225                if (get(i).getState() == LoaderTask.STATE_COMPLETED) {
226                    remove(i);
227                } else {
228                    i++;
229                }
230            }
231        }
232    }
233
234    /**
235     * Loader task.
236     * Each task is responsible for fetching child documents for the given parent document.
237     */
238    private static class LoaderTask {
239        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;
243        static final int STATE_CANCELLED = 4;
244
245        final MtpManager mManager;
246        final MtpDatabase mDatabase;
247        final int[] mOperationsSupported;
248        final Identifier mIdentifier;
249        int[] mObjectHandles;
250        int mState;
251        Date mLastNotified;
252        int mPosition;
253        IOException mError;
254
255        LoaderTask(MtpManager manager, MtpDatabase database, int[] operationsSupported,
256                Identifier identifier) {
257            assert operationsSupported != null;
258            assert identifier.mDocumentType != MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE;
259            mManager = manager;
260            mDatabase = database;
261            mOperationsSupported = operationsSupported;
262            mIdentifier = identifier;
263            mObjectHandles = null;
264            mState = STATE_START;
265            mPosition = 0;
266            mLastNotified = new Date();
267        }
268
269        synchronized void loadObjectHandles() {
270            assert mState == STATE_START;
271            mPosition = 0;
272            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
288        /**
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         */
293        synchronized Cursor createCursor(ContentResolver resolver, String[] columnNames)
294                throws IOException {
295            final Bundle extras = new Bundle();
296            switch (getState()) {
297                case STATE_LOADING:
298                    extras.putBoolean(DocumentsContract.EXTRA_LOADING, true);
299                    break;
300                case STATE_ERROR:
301                    throw mError;
302            }
303            final Cursor cursor =
304                    mDatabase.queryChildDocuments(columnNames, mIdentifier.mDocumentId);
305            cursor.setExtras(extras);
306            cursor.setNotificationUri(resolver, createUri());
307            return cursor;
308        }
309
310        /**
311         * Stores object information into database.
312         */
313        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            }
339            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            }
371            synchronized (this) {
372                // Check if the task is cancelled or not.
373                if (mState != STATE_LOADING) {
374                    return;
375                }
376                try {
377                    mDatabase.getMapper().putChildDocuments(
378                            mIdentifier.mDeviceId,
379                            mIdentifier.mDocumentId,
380                            mOperationsSupported,
381                            infoList.toArray(new MtpObjectInfo[infoList.size()]),
382                            objectSizeList);
383                } 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                }
401            }
402        }
403
404        /**
405         * Cancels the task.
406         */
407        synchronized void cancel() {
408            mDatabase.getMapper().cancelAddingDocuments(mIdentifier.mDocumentId);
409            mState = STATE_CANCELLED;
410        }
411
412        /**
413         * Returns a state of the task.
414         */
415        int getState() {
416            return mState;
417        }
418
419        /**
420         * Notifies a change of child list of the document.
421         */
422        void notify(ContentResolver resolver) {
423            resolver.notifyChange(createUri(), null, false);
424            mLastNotified = new Date();
425        }
426
427        private Uri createUri() {
428            return DocumentsContract.buildChildDocumentsUri(
429                    MtpDocumentsProvider.AUTHORITY, mIdentifier.mDocumentId);
430        }
431    }
432}
433