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 17 package com.android.shell; 18 19 import android.database.Cursor; 20 import android.database.MatrixCursor; 21 import android.database.MatrixCursor.RowBuilder; 22 import android.net.Uri; 23 import android.os.CancellationSignal; 24 import android.os.FileUtils; 25 import android.os.ParcelFileDescriptor; 26 import android.provider.DocumentsContract; 27 import android.provider.DocumentsContract.Document; 28 import android.provider.DocumentsContract.Root; 29 import android.provider.DocumentsProvider; 30 import android.support.provider.DocumentArchiveHelper; 31 import android.webkit.MimeTypeMap; 32 33 import java.io.File; 34 import java.io.FileNotFoundException; 35 36 public 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 getContext().getContentResolver().notifyChange(getNotificationUri(), null); 136 } 137 138 // This is used by BugreportProgressService so that the notification uri shared by 139 // BugreportProgressService and BugreportStorageProvider are guaranteed the same and unique 140 protected static Uri getNotificationUri() { 141 return DocumentsContract.buildChildDocumentsUri(AUTHORITY, DOC_ID_ROOT); 142 } 143 144 private static String[] resolveRootProjection(String[] projection) { 145 return projection != null ? projection : DEFAULT_ROOT_PROJECTION; 146 } 147 148 private static String[] resolveDocumentProjection(String[] projection) { 149 return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; 150 } 151 152 private static String getTypeForName(String name) { 153 final int lastDot = name.lastIndexOf('.'); 154 if (lastDot >= 0) { 155 final String extension = name.substring(lastDot + 1).toLowerCase(); 156 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 157 if (mime != null) { 158 return mime; 159 } 160 } 161 return "application/octet-stream"; 162 } 163 164 private String getDocIdForFile(File file) { 165 return DOC_ID_ROOT + ":" + file.getName(); 166 } 167 168 private File getFileForDocId(String documentId) throws FileNotFoundException { 169 final int splitIndex = documentId.indexOf(':', 1); 170 final String name = documentId.substring(splitIndex + 1); 171 if (splitIndex == -1 || !DOC_ID_ROOT.equals(documentId.substring(0, splitIndex)) || 172 !FileUtils.isValidExtFilename(name)) { 173 throw new FileNotFoundException("Invalid document ID: " + documentId); 174 } 175 final File file = new File(mRoot, name); 176 if (!file.exists()) { 177 throw new FileNotFoundException("File not found: " + documentId); 178 } 179 return file; 180 } 181 182 private void addFileRow(MatrixCursor result, File file) { 183 String mimeType = getTypeForName(file.getName()); 184 int flags = Document.FLAG_SUPPORTS_DELETE; 185 if (mArchiveHelper.isSupportedArchiveType(mimeType)) { 186 flags |= Document.FLAG_ARCHIVE; 187 } 188 189 final RowBuilder row = result.newRow(); 190 row.add(Document.COLUMN_DOCUMENT_ID, getDocIdForFile(file)); 191 row.add(Document.COLUMN_MIME_TYPE, mimeType); 192 row.add(Document.COLUMN_DISPLAY_NAME, file.getName()); 193 row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified()); 194 row.add(Document.COLUMN_FLAGS, flags); 195 row.add(Document.COLUMN_SIZE, file.length()); 196 } 197 } 198