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.shell;
18
19import android.database.Cursor;
20import android.database.MatrixCursor;
21import android.database.MatrixCursor.RowBuilder;
22import android.net.Uri;
23import android.os.CancellationSignal;
24import android.os.FileUtils;
25import android.os.ParcelFileDescriptor;
26import android.provider.DocumentsContract;
27import android.provider.DocumentsContract.Document;
28import android.provider.DocumentsContract.Root;
29import android.provider.DocumentsProvider;
30import android.support.provider.DocumentArchiveHelper;
31import android.webkit.MimeTypeMap;
32
33import java.io.File;
34import java.io.FileNotFoundException;
35
36public class BugreportStorageProvider extends DocumentsProvider {
37    private static final String AUTHORITY = "com.android.shell.documents";
38    private static final String DOC_ID_ROOT = "bugreport";
39
40    private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
41            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
42            Root.COLUMN_DOCUMENT_ID,
43    };
44
45    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
46            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
47            Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
48    };
49
50    private File mRoot;
51    private DocumentArchiveHelper mArchiveHelper;
52
53    @Override
54    public boolean onCreate() {
55        mRoot = new File(getContext().getFilesDir(), "bugreports");
56        mArchiveHelper = new DocumentArchiveHelper(this, (char) 0);
57        return true;
58    }
59
60    @Override
61    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
62        final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
63        final RowBuilder row = result.newRow();
64        row.add(Root.COLUMN_ROOT_ID, DOC_ID_ROOT);
65        row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED);
66        row.add(Root.COLUMN_ICON, android.R.mipmap.sym_def_app_icon);
67        row.add(Root.COLUMN_TITLE, getContext().getString(R.string.bugreport_storage_title));
68        row.add(Root.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
69        return result;
70    }
71
72    @Override
73    public Cursor queryDocument(String documentId, String[] projection)
74            throws FileNotFoundException {
75        if (mArchiveHelper.isArchivedDocument(documentId)) {
76            return mArchiveHelper.queryDocument(documentId, projection);
77        }
78
79        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
80        if (DOC_ID_ROOT.equals(documentId)) {
81            final RowBuilder row = result.newRow();
82            row.add(Document.COLUMN_DOCUMENT_ID, documentId);
83            row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
84            row.add(Document.COLUMN_DISPLAY_NAME, mRoot.getName());
85            row.add(Document.COLUMN_LAST_MODIFIED, mRoot.lastModified());
86            row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
87        } else {
88            addFileRow(result, getFileForDocId(documentId));
89        }
90        return result;
91    }
92
93    @Override
94    public Cursor queryChildDocuments(
95            String parentDocumentId, String[] projection, String sortOrder)
96            throws FileNotFoundException {
97        if (mArchiveHelper.isArchivedDocument(parentDocumentId) ||
98                mArchiveHelper.isSupportedArchiveType(getDocumentType(parentDocumentId))) {
99            return mArchiveHelper.queryChildDocuments(parentDocumentId, projection, sortOrder);
100        }
101
102        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
103        if (DOC_ID_ROOT.equals(parentDocumentId)) {
104            final File[] files = mRoot.listFiles();
105            if (files != null) {
106                for (File file : files) {
107                    addFileRow(result, file);
108                }
109                result.setNotificationUri(getContext().getContentResolver(), getNotificationUri());
110            }
111        }
112        return result;
113    }
114
115    @Override
116    public ParcelFileDescriptor openDocument(
117            String documentId, String mode, CancellationSignal signal)
118            throws FileNotFoundException {
119        if (mArchiveHelper.isArchivedDocument(documentId)) {
120            return mArchiveHelper.openDocument(documentId, mode, signal);
121        }
122
123        if (ParcelFileDescriptor.parseMode(mode) != ParcelFileDescriptor.MODE_READ_ONLY) {
124            throw new FileNotFoundException("Failed to open: " + documentId + ", mode = " + mode);
125        }
126        return ParcelFileDescriptor.open(getFileForDocId(documentId),
127                ParcelFileDescriptor.MODE_READ_ONLY);
128    }
129
130    @Override
131    public void deleteDocument(String documentId) throws FileNotFoundException {
132        if (!getFileForDocId(documentId).delete()) {
133            throw new FileNotFoundException("Failed to delete: " + documentId);
134        }
135    }
136
137    // This is used by BugreportProgressService so that the notification uri shared by
138    // BugreportProgressService and BugreportStorageProvider are guaranteed the same and unique
139    protected static Uri getNotificationUri() {
140      return DocumentsContract.buildChildDocumentsUri(AUTHORITY, DOC_ID_ROOT);
141    }
142
143    private static String[] resolveRootProjection(String[] projection) {
144        return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
145    }
146
147    private static String[] resolveDocumentProjection(String[] projection) {
148        return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
149    }
150
151    private static String getTypeForName(String name) {
152        final int lastDot = name.lastIndexOf('.');
153        if (lastDot >= 0) {
154            final String extension = name.substring(lastDot + 1).toLowerCase();
155            final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
156            if (mime != null) {
157                return mime;
158            }
159        }
160        return "application/octet-stream";
161    }
162
163    private String getDocIdForFile(File file) {
164        return DOC_ID_ROOT + ":" + file.getName();
165    }
166
167    private File getFileForDocId(String documentId) throws FileNotFoundException {
168        final int splitIndex = documentId.indexOf(':', 1);
169        final String name = documentId.substring(splitIndex + 1);
170        if (splitIndex == -1 || !DOC_ID_ROOT.equals(documentId.substring(0, splitIndex)) ||
171                !FileUtils.isValidExtFilename(name)) {
172            throw new FileNotFoundException("Invalid document ID: " + documentId);
173        }
174        final File file = new File(mRoot, name);
175        if (!file.exists()) {
176            throw new FileNotFoundException("File not found: " + documentId);
177        }
178        return file;
179    }
180
181    private void addFileRow(MatrixCursor result, File file) {
182        String mimeType = getTypeForName(file.getName());
183        int flags = Document.FLAG_SUPPORTS_DELETE;
184        if (mArchiveHelper.isSupportedArchiveType(mimeType)) {
185            flags |= Document.FLAG_ARCHIVE;
186        }
187
188        final RowBuilder row = result.newRow();
189        row.add(Document.COLUMN_DOCUMENT_ID, getDocIdForFile(file));
190        row.add(Document.COLUMN_MIME_TYPE, mimeType);
191        row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
192        row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
193        row.add(Document.COLUMN_FLAGS, flags);
194        row.add(Document.COLUMN_SIZE, file.length());
195    }
196}
197