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.os.CancellationSignal;
23import android.os.FileUtils;
24import android.os.ParcelFileDescriptor;
25import android.provider.DocumentsContract.Document;
26import android.provider.DocumentsContract.Root;
27import android.provider.DocumentsProvider;
28import android.webkit.MimeTypeMap;
29
30import java.io.File;
31import java.io.FileNotFoundException;
32
33public class BugreportStorageProvider extends DocumentsProvider {
34    private static final String DOC_ID_ROOT = "bugreport";
35
36    private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
37            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
38            Root.COLUMN_DOCUMENT_ID,
39    };
40
41    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
42            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
43            Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
44    };
45
46    private File mRoot;
47
48    @Override
49    public boolean onCreate() {
50        mRoot = new File(getContext().getFilesDir(), "bugreports");
51        return true;
52    }
53
54    @Override
55    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
56        final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
57        final RowBuilder row = result.newRow();
58        row.add(Root.COLUMN_ROOT_ID, DOC_ID_ROOT);
59        row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED);
60        row.add(Root.COLUMN_ICON, android.R.mipmap.sym_def_app_icon);
61        row.add(Root.COLUMN_TITLE, getContext().getString(R.string.bugreport_storage_title));
62        row.add(Root.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
63        return result;
64    }
65
66    @Override
67    public Cursor queryDocument(String documentId, String[] projection)
68            throws FileNotFoundException {
69        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
70        if (DOC_ID_ROOT.equals(documentId)) {
71            final RowBuilder row = result.newRow();
72            row.add(Document.COLUMN_DOCUMENT_ID, documentId);
73            row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
74            row.add(Document.COLUMN_DISPLAY_NAME, mRoot.getName());
75            row.add(Document.COLUMN_LAST_MODIFIED, mRoot.lastModified());
76            row.add(Document.COLUMN_FLAGS, Document.FLAG_DIR_PREFERS_LAST_MODIFIED);
77        } else {
78            addFileRow(result, getFileForDocId(documentId));
79        }
80        return result;
81    }
82
83    @Override
84    public Cursor queryChildDocuments(
85            String parentDocumentId, String[] projection, String sortOrder)
86            throws FileNotFoundException {
87        final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
88        if (DOC_ID_ROOT.equals(parentDocumentId)) {
89            final File[] files = mRoot.listFiles();
90            if (files != null) {
91                for (File file : files) {
92                    addFileRow(result, file);
93                }
94            }
95        }
96        return result;
97    }
98
99    @Override
100    public ParcelFileDescriptor openDocument(
101            String documentId, String mode, CancellationSignal signal)
102            throws FileNotFoundException {
103        if (ParcelFileDescriptor.parseMode(mode) != ParcelFileDescriptor.MODE_READ_ONLY) {
104            throw new FileNotFoundException("Failed to open: " + documentId + ", mode = " + mode);
105        }
106        return ParcelFileDescriptor.open(getFileForDocId(documentId),
107                ParcelFileDescriptor.MODE_READ_ONLY);
108    }
109
110    @Override
111    public void deleteDocument(String documentId) throws FileNotFoundException {
112        if (!getFileForDocId(documentId).delete()) {
113            throw new FileNotFoundException("Failed to delete: " + documentId);
114        }
115    }
116
117    private static String[] resolveRootProjection(String[] projection) {
118        return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
119    }
120
121    private static String[] resolveDocumentProjection(String[] projection) {
122        return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
123    }
124
125    private static String getTypeForName(String name) {
126        final int lastDot = name.lastIndexOf('.');
127        if (lastDot >= 0) {
128            final String extension = name.substring(lastDot + 1).toLowerCase();
129            final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
130            if (mime != null) {
131                return mime;
132            }
133        }
134        return "application/octet-stream";
135    }
136
137    private String getDocIdForFile(File file) {
138        return DOC_ID_ROOT + ":" + file.getName();
139    }
140
141    private File getFileForDocId(String documentId) throws FileNotFoundException {
142        final int splitIndex = documentId.indexOf(':', 1);
143        final String name = documentId.substring(splitIndex + 1);
144        if (splitIndex == -1 || !DOC_ID_ROOT.equals(documentId.substring(0, splitIndex)) ||
145                !FileUtils.isValidExtFilename(name)) {
146            throw new FileNotFoundException("Invalid document ID: " + documentId);
147        }
148        final File file = new File(mRoot, name);
149        if (!file.exists()) {
150            throw new FileNotFoundException("File not found: " + documentId);
151        }
152        return file;
153    }
154
155    private void addFileRow(MatrixCursor result, File file) {
156        final RowBuilder row = result.newRow();
157        row.add(Document.COLUMN_DOCUMENT_ID, getDocIdForFile(file));
158        row.add(Document.COLUMN_MIME_TYPE, getTypeForName(file.getName()));
159        row.add(Document.COLUMN_DISPLAY_NAME, file.getName());
160        row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
161        row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE);
162        row.add(Document.COLUMN_SIZE, file.length());
163    }
164}
165