1 /* 2 * Copyright (C) 2019 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.providers.media; 18 19 import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME; 20 import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME; 21 22 import android.content.ContentProvider; 23 import android.content.ContentProviderOperation; 24 import android.content.ContentProviderResult; 25 import android.content.ContentUris; 26 import android.content.ContentValues; 27 import android.content.Context; 28 import android.content.OperationApplicationException; 29 import android.content.pm.ProviderInfo; 30 import android.database.Cursor; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.provider.MediaStore; 34 import android.provider.MediaStore.MediaColumns; 35 import android.util.ArraySet; 36 37 import androidx.annotation.NonNull; 38 39 import com.android.providers.media.util.Logging; 40 41 import java.io.File; 42 import java.io.FileDescriptor; 43 import java.io.IOException; 44 import java.io.PrintWriter; 45 import java.util.ArrayList; 46 import java.util.Objects; 47 import java.util.Set; 48 49 /** 50 * Very limited subset of {@link MediaProvider} which only surfaces 51 * {@link android.provider.MediaStore.Files} data. 52 */ 53 public class LegacyMediaProvider extends ContentProvider { 54 private DatabaseHelper mInternalDatabase; 55 private DatabaseHelper mExternalDatabase; 56 57 public static final String START_LEGACY_MIGRATION_CALL = "start_legacy_migration"; 58 public static final String FINISH_LEGACY_MIGRATION_CALL = "finish_legacy_migration"; 59 60 @Override attachInfo(Context context, ProviderInfo info)61 public void attachInfo(Context context, ProviderInfo info) { 62 // Sanity check our setup 63 if (!info.exported) { 64 throw new SecurityException("Provider must be exported"); 65 } 66 if (!android.Manifest.permission.WRITE_MEDIA_STORAGE.equals(info.readPermission) 67 || !android.Manifest.permission.WRITE_MEDIA_STORAGE.equals(info.writePermission)) { 68 throw new SecurityException("Provider must be protected by WRITE_MEDIA_STORAGE"); 69 } 70 71 super.attachInfo(context, info); 72 } 73 74 @Override onCreate()75 public boolean onCreate() { 76 final Context context = getContext(); 77 78 final File persistentDir = context.getDir("logs", Context.MODE_PRIVATE); 79 Logging.initPersistent(persistentDir); 80 81 mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, 82 true, false, true, null, null, null, null, null); 83 mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, 84 false, false, true, null, null, null, null, null); 85 86 return true; 87 } 88 getDatabaseForUri(Uri uri)89 private @NonNull DatabaseHelper getDatabaseForUri(Uri uri) { 90 final String volumeName = MediaStore.getVolumeName(uri); 91 switch (volumeName) { 92 case MediaStore.VOLUME_INTERNAL: 93 return Objects.requireNonNull(mInternalDatabase, "Missing internal database"); 94 default: 95 return Objects.requireNonNull(mExternalDatabase, "Missing external database"); 96 } 97 } 98 99 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)100 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 101 String sortOrder) { 102 final DatabaseHelper helper = getDatabaseForUri(uri); 103 return helper.runWithoutTransaction((db) -> { 104 return db.query("files", projection, selection, selectionArgs, 105 null, null, sortOrder); 106 }); 107 } 108 109 @Override getType(Uri uri)110 public String getType(Uri uri) { 111 throw new UnsupportedOperationException(); 112 } 113 114 @Override applyBatch(ArrayList<ContentProviderOperation> operations)115 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 116 throws OperationApplicationException { 117 // Open transactions on databases for requested volumes 118 final Set<DatabaseHelper> transactions = new ArraySet<>(); 119 try { 120 for (ContentProviderOperation op : operations) { 121 final DatabaseHelper helper = getDatabaseForUri(op.getUri()); 122 if (!transactions.contains(helper)) { 123 helper.beginTransaction(); 124 transactions.add(helper); 125 } 126 } 127 128 final ContentProviderResult[] result = super.applyBatch(operations); 129 for (DatabaseHelper helper : transactions) { 130 helper.setTransactionSuccessful(); 131 } 132 return result; 133 } finally { 134 for (DatabaseHelper helper : transactions) { 135 helper.endTransaction(); 136 } 137 } 138 } 139 140 @Override insert(Uri uri, ContentValues values)141 public Uri insert(Uri uri, ContentValues values) { 142 if (!uri.getBooleanQueryParameter("silent", false)) { 143 try { 144 final File file = new File(values.getAsString(MediaColumns.DATA)); 145 file.getParentFile().mkdirs(); 146 file.createNewFile(); 147 } catch (IOException e) { 148 throw new IllegalStateException(e); 149 } 150 } 151 152 final DatabaseHelper helper = getDatabaseForUri(uri); 153 final long id = helper.runWithTransaction((db) -> { 154 return db.insert("files", null, values); 155 }); 156 return ContentUris.withAppendedId(uri, id); 157 } 158 159 @Override delete(Uri uri, String selection, String[] selectionArgs)160 public int delete(Uri uri, String selection, String[] selectionArgs) { 161 throw new UnsupportedOperationException(); 162 } 163 164 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)165 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 166 throw new UnsupportedOperationException(); 167 } 168 169 @Override call(String authority, String method, String arg, Bundle extras)170 public Bundle call(String authority, String method, String arg, Bundle extras) { 171 switch (method) { 172 case START_LEGACY_MIGRATION_CALL: { 173 // Nice to know, but nothing actionable 174 break; 175 } 176 case FINISH_LEGACY_MIGRATION_CALL: { 177 // We're only going to hear this once, since we've either 178 // successfully migrated legacy data, or we're never going to 179 // try again, so it's time to clean things up 180 final String volumeName = arg; 181 switch (volumeName) { 182 case MediaStore.VOLUME_INTERNAL: { 183 mInternalDatabase.close(); 184 getContext().deleteDatabase(INTERNAL_DATABASE_NAME); 185 break; 186 } 187 default: { 188 mExternalDatabase.close(); 189 getContext().deleteDatabase(EXTERNAL_DATABASE_NAME); 190 break; 191 } 192 } 193 } 194 } 195 return null; 196 } 197 198 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)199 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 200 Logging.dumpPersistent(writer); 201 } 202 } 203