DocumentLoader.java revision 619afdaae1ec7dcbd71bb1f698a0901a1fa290fe
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.content.ContentResolver;
20import android.database.Cursor;
21import android.mtp.MtpObjectInfo;
22import android.net.Uri;
23import android.os.Bundle;
24import android.os.Process;
25import android.provider.DocumentsContract;
26import android.util.Log;
27
28import java.io.FileNotFoundException;
29import java.io.IOException;
30import java.util.ArrayList;
31import java.util.Arrays;
32import java.util.Date;
33import java.util.LinkedList;
34
35/**
36 * Loader for MTP document.
37 * At the first request, the loader returns only first NUM_INITIAL_ENTRIES. Then it launches
38 * background thread to load the rest documents and caches its result for next requests.
39 * TODO: Rename this class to ObjectInfoLoader
40 */
41class DocumentLoader {
42    static final int NUM_INITIAL_ENTRIES = 10;
43    static final int NUM_LOADING_ENTRIES = 20;
44    static final int NOTIFY_PERIOD_MS = 500;
45
46    private final MtpManager mMtpManager;
47    private final ContentResolver mResolver;
48    private final MtpDatabase mDatabase;
49    private final TaskList mTaskList = new TaskList();
50    private boolean mHasBackgroundThread = false;
51
52    DocumentLoader(MtpManager mtpManager, ContentResolver resolver, MtpDatabase database) {
53        mMtpManager = mtpManager;
54        mResolver = resolver;
55        mDatabase = database;
56    }
57
58    private static MtpObjectInfo[] loadDocuments(MtpManager manager, int deviceId, int[] handles)
59            throws IOException {
60        final ArrayList<MtpObjectInfo> objects = new ArrayList<>();
61        for (int i = 0; i < handles.length; i++) {
62            final MtpObjectInfo info = manager.getObjectInfo(deviceId, handles[i]);
63            if (info == null) {
64                Log.e(MtpDocumentsProvider.TAG,
65                        "Failed to obtain object info handle=" + handles[i]);
66                continue;
67            }
68            objects.add(info);
69        }
70        return objects.toArray(new MtpObjectInfo[objects.size()]);
71    }
72
73    synchronized Cursor queryChildDocuments(String[] columnNames, Identifier parent)
74            throws IOException {
75        LoaderTask task = mTaskList.findTask(parent);
76        if (task == null) {
77            if (parent.mDocumentId == null) {
78                throw new FileNotFoundException("Parent not found.");
79            }
80
81            int parentHandle = parent.mObjectHandle;
82            // Need to pass the special value MtpManager.OBJECT_HANDLE_ROOT_CHILDREN to
83            // getObjectHandles if we would like to obtain children under the root.
84            if (parent.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) {
85                parentHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN;
86            }
87            // TODO: Handle nit race around here.
88            // 1. getObjectHandles.
89            // 2. putNewDocument.
90            // 3. startAddingChildDocuemnts.
91            // 4. stopAddingChildDocuments - It removes the new document added at the step 2,
92            //     because it is not updated between start/stopAddingChildDocuments.
93            task = new LoaderTask(mDatabase, parent, mMtpManager.getObjectHandles(
94                    parent.mDeviceId, parent.mStorageId, parentHandle));
95            task.fillDocuments(loadDocuments(
96                    mMtpManager,
97                    parent.mDeviceId,
98                    task.getUnloadedObjectHandles(NUM_INITIAL_ENTRIES)));
99        } else {
100            // Once remove the existing task in order to add it to the head of the list.
101            mTaskList.remove(task);
102        }
103
104        mTaskList.addFirst(task);
105        if (task.getState() == LoaderTask.STATE_LOADING && !mHasBackgroundThread) {
106            mHasBackgroundThread = true;
107            new BackgroundLoaderThread().start();
108        }
109        return task.createCursor(mResolver, columnNames);
110    }
111
112    synchronized void clearTasks() {
113        mTaskList.clear();
114    }
115
116    synchronized void clearCompletedTasks() {
117        mTaskList.clearCompletedTasks();
118    }
119
120    synchronized void clearTask(Identifier parentIdentifier) {
121        mTaskList.clearTask(parentIdentifier);
122    }
123
124    private class BackgroundLoaderThread extends Thread {
125        @Override
126        public void run() {
127            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
128            while (true) {
129                LoaderTask task;
130                int deviceId;
131                int[] handles;
132                synchronized (DocumentLoader.this) {
133                    task = mTaskList.findRunningTask();
134                    if (task == null) {
135                        mHasBackgroundThread = false;
136                        return;
137                    }
138                    deviceId = task.mIdentifier.mDeviceId;
139                    handles = task.getUnloadedObjectHandles(NUM_LOADING_ENTRIES);
140                }
141
142                try {
143                    final MtpObjectInfo[] objectInfos =
144                            loadDocuments(mMtpManager, deviceId, handles);
145                    task.fillDocuments(objectInfos);
146                    final boolean shouldNotify =
147                            task.mLastNotified.getTime() <
148                            new Date().getTime() - NOTIFY_PERIOD_MS ||
149                            task.getState() != LoaderTask.STATE_LOADING;
150                    if (shouldNotify) {
151                        task.notify(mResolver);
152                    }
153                } catch (IOException exception) {
154                    task.setError(exception);
155                }
156            }
157        }
158    }
159
160    private static class TaskList extends LinkedList<LoaderTask> {
161        LoaderTask findTask(Identifier parent) {
162            for (int i = 0; i < size(); i++) {
163                if (get(i).mIdentifier.equals(parent))
164                    return get(i);
165            }
166            return null;
167        }
168
169        LoaderTask findRunningTask() {
170            for (int i = 0; i < size(); i++) {
171                if (get(i).getState() == LoaderTask.STATE_LOADING)
172                    return get(i);
173            }
174            return null;
175        }
176
177        void clearCompletedTasks() {
178            int i = 0;
179            while (i < size()) {
180                if (get(i).getState() == LoaderTask.STATE_COMPLETED) {
181                    remove(i);
182                } else {
183                    i++;
184                }
185            }
186        }
187
188        void clearTask(Identifier parentIdentifier) {
189            for (int i = 0; i < size(); i++) {
190                final LoaderTask task = get(i);
191                if (task.mIdentifier.mDeviceId == parentIdentifier.mDeviceId &&
192                        task.mIdentifier.mObjectHandle == parentIdentifier.mObjectHandle) {
193                    remove(i);
194                    return;
195                }
196            }
197        }
198    }
199
200    private static class LoaderTask {
201        static final int STATE_LOADING = 0;
202        static final int STATE_COMPLETED = 1;
203        static final int STATE_ERROR = 2;
204
205        final MtpDatabase mDatabase;
206        final Identifier mIdentifier;
207        final int[] mObjectHandles;
208        Date mLastNotified;
209        int mNumLoaded;
210        Exception mError;
211
212        LoaderTask(MtpDatabase database, Identifier identifier, int[] objectHandles) {
213            mDatabase = database;
214            mIdentifier = identifier;
215            mObjectHandles = objectHandles;
216            mNumLoaded = 0;
217            mLastNotified = new Date();
218        }
219
220        Cursor createCursor(ContentResolver resolver, String[] columnNames) throws IOException {
221            final Bundle extras = new Bundle();
222            switch (getState()) {
223                case STATE_LOADING:
224                    extras.putBoolean(DocumentsContract.EXTRA_LOADING, true);
225                    break;
226                case STATE_ERROR:
227                    throw new IOException(mError);
228            }
229
230            final Cursor cursor =
231                    mDatabase.queryChildDocuments(columnNames, mIdentifier.mDocumentId);
232            cursor.setNotificationUri(resolver, createUri());
233            cursor.respond(extras);
234
235            return cursor;
236        }
237
238        int getState() {
239            if (mError != null) {
240                return STATE_ERROR;
241            } else if (mNumLoaded == mObjectHandles.length) {
242                return STATE_COMPLETED;
243            } else {
244                return STATE_LOADING;
245            }
246        }
247
248        int[] getUnloadedObjectHandles(int count) {
249            return Arrays.copyOfRange(
250                    mObjectHandles,
251                    mNumLoaded,
252                    Math.min(mNumLoaded + count, mObjectHandles.length));
253        }
254
255        void notify(ContentResolver resolver) {
256            resolver.notifyChange(createUri(), null, false);
257            mLastNotified = new Date();
258        }
259
260        void fillDocuments(MtpObjectInfo[] objectInfoList) {
261            if (objectInfoList.length == 0 || getState() != STATE_LOADING) {
262                return;
263            }
264            try{
265                if (mNumLoaded == 0) {
266                    mDatabase.getMapper().startAddingDocuments(mIdentifier.mDocumentId);
267                }
268                mDatabase.getMapper().putChildDocuments(
269                        mIdentifier.mDeviceId, mIdentifier.mDocumentId, objectInfoList);
270                mNumLoaded += objectInfoList.length;
271                if (getState() != STATE_LOADING) {
272                    mDatabase.getMapper().stopAddingDocuments(mIdentifier.mDocumentId);
273                }
274            } catch (FileNotFoundException exception) {
275                setErrorInternal(exception);
276            }
277        }
278
279        void setError(Exception error) {
280            final int lastState = getState();
281            setErrorInternal(error);
282            if (lastState == STATE_LOADING) {
283                try {
284                    mDatabase.getMapper().stopAddingDocuments(mIdentifier.mDocumentId);
285                } catch (FileNotFoundException exception) {
286                    setErrorInternal(exception);
287                }
288            }
289        }
290
291        private void setErrorInternal(Exception error) {
292            Log.e(MtpDocumentsProvider.TAG, "Error in DocumentLoader thread", error);
293            mError = error;
294            mNumLoaded = 0;
295        }
296
297        private Uri createUri() {
298            return DocumentsContract.buildChildDocumentsUri(
299                    MtpDocumentsProvider.AUTHORITY, mIdentifier.mDocumentId);
300        }
301    }
302}
303