1/*
2 * Copyright (C) 2016 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.documentsui;
18
19import android.content.Context;
20import android.content.pm.ProviderInfo;
21import android.content.res.AssetFileDescriptor;
22import android.database.Cursor;
23import android.database.MatrixCursor.RowBuilder;
24import android.database.MatrixCursor;
25import android.graphics.Point;
26import android.os.CancellationSignal;
27import android.os.FileUtils;
28import android.os.ParcelFileDescriptor;
29import android.provider.DocumentsContract.Document;
30import android.provider.DocumentsContract.Root;
31import android.provider.DocumentsContract;
32import android.provider.DocumentsProvider;
33
34import java.io.File;
35import java.io.FileNotFoundException;
36import java.io.IOException;
37import java.util.ArrayList;
38import java.util.HashMap;
39import java.util.List;
40import java.util.Map;
41import java.util.Random;
42
43/**
44 * Provider with thousands of files for testing loading time of directories in DocumentsUI.
45 * It doesn't support any file operations.
46 */
47public class StressProvider extends DocumentsProvider {
48
49    public static final String DEFAULT_AUTHORITY = "com.android.documentsui.stressprovider";
50
51    // Empty root.
52    public static final String STRESS_ROOT_0_ID = "STRESS_ROOT_0";
53
54    // Root with thousands of directories.
55    public static final String STRESS_ROOT_1_ID = "STRESS_ROOT_1";
56
57    // Root with hundreds of files.
58    public static final String STRESS_ROOT_2_ID = "STRESS_ROOT_2";
59
60    private static final String STRESS_ROOT_0_DOC_ID = "STRESS_ROOT_0_DOC";
61    private static final String STRESS_ROOT_1_DOC_ID = "STRESS_ROOT_1_DOC";
62    private static final String STRESS_ROOT_2_DOC_ID = "STRESS_ROOT_2_DOC";
63
64    private static final int STRESS_ROOT_1_ITEMS = 10000;
65    private static final int STRESS_ROOT_2_ITEMS = 300;
66
67    private static final String MIME_TYPE_IMAGE = "image/jpeg";
68    private static final long REFERENCE_TIMESTAMP = 1459159369359L;
69
70    private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
71            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
72            Root.COLUMN_AVAILABLE_BYTES
73    };
74    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
75            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
76            Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
77    };
78
79    private String mAuthority = DEFAULT_AUTHORITY;
80
81    // Map from a root document id to children document ids.
82    private Map<String, ArrayList<StubDocument>> mChildDocuments = new HashMap<>();
83
84    private Map<String, StubDocument> mDocuments = new HashMap<>();
85    private Map<String, StubRoot> mRoots = new HashMap<>();
86
87    @Override
88    public void attachInfo(Context context, ProviderInfo info) {
89        mAuthority = info.authority;
90        super.attachInfo(context, info);
91    }
92
93    @Override
94    public boolean onCreate() {
95        StubDocument document;
96
97        ArrayList<StubDocument> children = new ArrayList<StubDocument>();
98        mChildDocuments.put(STRESS_ROOT_1_DOC_ID, children);
99        for (int i = 0; i < STRESS_ROOT_1_ITEMS; i++) {
100            document = StubDocument.createDirectory(i);
101            mDocuments.put(document.id, document);
102            children.add(document);
103        }
104
105        children = new ArrayList<StubDocument>();
106        mChildDocuments.put(STRESS_ROOT_2_DOC_ID, children);
107        for (int i = 0; i < STRESS_ROOT_2_ITEMS; i++) {
108            try {
109                document = StubDocument.createFile(
110                        getContext(), MIME_TYPE_IMAGE,
111                        com.android.documentsui.perftests.R.raw.earth_small,
112                        STRESS_ROOT_1_ITEMS + i);
113            } catch (IOException e) {
114                return false;
115            }
116            mDocuments.put(document.id, document);
117            children.add(document);
118        }
119
120        mRoots.put(STRESS_ROOT_0_ID, new StubRoot(STRESS_ROOT_0_ID, STRESS_ROOT_0_DOC_ID));
121        mRoots.put(STRESS_ROOT_1_ID, new StubRoot(STRESS_ROOT_1_ID, STRESS_ROOT_1_DOC_ID));
122        mRoots.put(STRESS_ROOT_2_ID, new StubRoot(STRESS_ROOT_2_ID, STRESS_ROOT_2_DOC_ID));
123
124        mDocuments.put(STRESS_ROOT_0_DOC_ID, StubDocument.createDirectory(STRESS_ROOT_0_DOC_ID));
125        mDocuments.put(STRESS_ROOT_1_DOC_ID, StubDocument.createDirectory(STRESS_ROOT_1_DOC_ID));
126        mDocuments.put(STRESS_ROOT_2_DOC_ID, StubDocument.createDirectory(STRESS_ROOT_2_DOC_ID));
127
128        return true;
129    }
130
131    @Override
132    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
133        final MatrixCursor result = new MatrixCursor(DEFAULT_ROOT_PROJECTION);
134        for (StubRoot root : mRoots.values()) {
135            includeRoot(result, root);
136        }
137        return result;
138    }
139
140    @Override
141    public Cursor queryDocument(String documentId, String[] projection)
142            throws FileNotFoundException {
143        final MatrixCursor result = new MatrixCursor(DEFAULT_DOCUMENT_PROJECTION);
144        final StubDocument document = mDocuments.get(documentId);
145        includeDocument(result, document);
146        return result;
147    }
148
149    @Override
150    public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
151            String sortOrder)
152            throws FileNotFoundException {
153        final MatrixCursor result = new MatrixCursor(DEFAULT_DOCUMENT_PROJECTION);
154        final ArrayList<StubDocument> childDocuments = mChildDocuments.get(parentDocumentId);
155        if (childDocuments != null) {
156            for (StubDocument document : childDocuments) {
157                includeDocument(result, document);
158            }
159        }
160        return result;
161    }
162
163    @Override
164    public AssetFileDescriptor openDocumentThumbnail(String docId, Point sizeHint,
165            CancellationSignal signal)
166            throws FileNotFoundException {
167        final StubDocument document = mDocuments.get(docId);
168        return getContext().getResources().openRawResourceFd(document.thumbnail);
169    }
170
171    @Override
172    public ParcelFileDescriptor openDocument(String docId, String mode,
173            CancellationSignal signal)
174            throws FileNotFoundException {
175        throw new UnsupportedOperationException();
176    }
177
178    private void includeRoot(MatrixCursor result, StubRoot root) {
179        final RowBuilder row = result.newRow();
180        row.add(Root.COLUMN_ROOT_ID, root.id);
181        row.add(Root.COLUMN_FLAGS, 0);
182        row.add(Root.COLUMN_TITLE, root.id);
183        row.add(Root.COLUMN_DOCUMENT_ID, root.documentId);
184    }
185
186    private void includeDocument(MatrixCursor result, StubDocument document) {
187        final RowBuilder row = result.newRow();
188        row.add(Document.COLUMN_DOCUMENT_ID, document.id);
189        row.add(Document.COLUMN_DISPLAY_NAME, document.id);
190        row.add(Document.COLUMN_SIZE, document.size);
191        row.add(Document.COLUMN_MIME_TYPE, document.mimeType);
192        row.add(Document.COLUMN_FLAGS,
193                document.thumbnail != -1 ? Document.FLAG_SUPPORTS_THUMBNAIL : 0);
194        row.add(Document.COLUMN_LAST_MODIFIED, document.lastModified);
195    }
196
197    private static String getStubDocumentIdForFile(File file) {
198        return file.getAbsolutePath();
199    }
200
201    private static class StubDocument {
202        final String mimeType;
203        final String id;
204        final int size;
205        final long lastModified;
206        final int thumbnail;
207
208        private StubDocument(String mimeType, String id, int size, long lastModified,
209                int thumbnail) {
210            this.mimeType = mimeType;
211            this.id = id;
212            this.size = size;
213            this.lastModified = lastModified;
214            this.thumbnail = thumbnail;
215        }
216
217        public static StubDocument createDirectory(int index) {
218            return new StubDocument(
219                    DocumentsContract.Document.MIME_TYPE_DIR, createRandomId(index), 0,
220                    createRandomTime(index), -1);
221        }
222
223        public static StubDocument createDirectory(String id) {
224            return new StubDocument(DocumentsContract.Document.MIME_TYPE_DIR, id, 0, 0, -1);
225        }
226
227        public static StubDocument createFile(Context context, String mimeType, int thumbnail,
228                int index) throws IOException {
229            return new StubDocument(
230                    mimeType, createRandomId(index), createRandomSize(index),
231                    createRandomTime(index), thumbnail);
232        }
233
234        private static String createRandomId(int index) {
235            final Random random = new Random(index);
236            final StringBuilder builder = new StringBuilder();
237            for (int i = 0; i < 20; i++) {
238                builder.append((char) (random.nextInt(96) + 32));
239            }
240            builder.append(index);  // Append a number to guarantee uniqueness.
241            return builder.toString();
242        }
243
244        private static int createRandomSize(int index) {
245            final Random random = new Random(index);
246            return random.nextInt(1024 * 1024 * 100);  // Up to 100 MB.
247        }
248
249        private static long createRandomTime(int index) {
250            final Random random = new Random(index);
251            // Up to 30 days backwards from REFERENCE_TIMESTAMP.
252            return REFERENCE_TIMESTAMP - random.nextLong() % 1000L * 60 * 60 * 24 * 30;
253        }
254    }
255
256    private static class StubRoot {
257        final String id;
258        final String documentId;
259
260        public StubRoot(String id, String documentId) {
261            this.id = id;
262            this.documentId = documentId;
263        }
264    }
265}
266