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.LegacyDatabaseHelper.EXTERNAL_DATABASE_NAME; 20 import static com.android.providers.media.LegacyDatabaseHelper.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.UriMatcher; 30 import android.content.pm.ProviderInfo; 31 import android.database.Cursor; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.provider.MediaStore; 35 import android.provider.MediaStore.MediaColumns; 36 import android.util.ArraySet; 37 38 import androidx.annotation.NonNull; 39 40 import com.android.providers.media.util.Logging; 41 42 import java.io.File; 43 import java.io.FileDescriptor; 44 import java.io.IOException; 45 import java.io.PrintWriter; 46 import java.util.ArrayList; 47 import java.util.Objects; 48 import java.util.Set; 49 50 /** 51 * Very limited subset of {@link MediaProvider} which only surfaces 52 * {@link android.provider.MediaStore.Files} data. 53 */ 54 public class LegacyMediaProvider extends ContentProvider { 55 private LegacyDatabaseHelper mInternalDatabase; 56 private LegacyDatabaseHelper mExternalDatabase; 57 58 public static final String START_LEGACY_MIGRATION_CALL = "start_legacy_migration"; 59 public static final String FINISH_LEGACY_MIGRATION_CALL = "finish_legacy_migration"; 60 61 @Override attachInfo(Context context, ProviderInfo info)62 public void attachInfo(Context context, ProviderInfo info) { 63 // Sanity check our setup 64 if (!info.exported) { 65 throw new SecurityException("Provider must be exported"); 66 } 67 if (!android.Manifest.permission.WRITE_MEDIA_STORAGE.equals(info.readPermission) 68 || !android.Manifest.permission.WRITE_MEDIA_STORAGE.equals(info.writePermission)) { 69 throw new SecurityException("Provider must be protected by WRITE_MEDIA_STORAGE"); 70 } 71 72 super.attachInfo(context, info); 73 } 74 75 @Override onCreate()76 public boolean onCreate() { 77 final Context context = getContext(); 78 79 final File persistentDir = context.getDir("logs", Context.MODE_PRIVATE); 80 Logging.initPersistent(persistentDir); 81 82 mInternalDatabase = new LegacyDatabaseHelper(context, INTERNAL_DATABASE_NAME, true); 83 mExternalDatabase = new LegacyDatabaseHelper(context, EXTERNAL_DATABASE_NAME, true); 84 85 return true; 86 } 87 getDatabaseForUri(Uri uri)88 private @NonNull LegacyDatabaseHelper getDatabaseForUri(Uri uri) { 89 final String volumeName = MediaStore.getVolumeName(uri); 90 switch (volumeName) { 91 case MediaStore.VOLUME_INTERNAL: 92 return Objects.requireNonNull(mInternalDatabase, "Missing internal database"); 93 default: 94 return Objects.requireNonNull(mExternalDatabase, "Missing external database"); 95 } 96 } 97 98 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)99 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 100 String sortOrder) { 101 final String appendedSelection = getAppendedSelection(selection, uri); 102 final LegacyDatabaseHelper helper = getDatabaseForUri(uri); 103 return helper.runWithoutTransaction((db) -> { 104 return db.query(getTableName(uri), projection, appendedSelection, 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<LegacyDatabaseHelper> transactions = new ArraySet<>(); 119 try { 120 for (ContentProviderOperation op : operations) { 121 final LegacyDatabaseHelper 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 (LegacyDatabaseHelper helper : transactions) { 130 helper.setTransactionSuccessful(); 131 } 132 return result; 133 } finally { 134 for (LegacyDatabaseHelper 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 LegacyDatabaseHelper helper = getDatabaseForUri(uri); 153 final long id = helper.runWithTransaction((db) -> { 154 return db.insert(getTableName(uri), 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 private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112; 170 private static final int FILES_ID = 701; 171 private static final UriMatcher BASIC_URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); 172 static { 173 final UriMatcher basicUriMatcher = BASIC_URI_MATCHER; basicUriMatcher.addURI(MediaStore.AUTHORITY_LEGACY, "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS)174 basicUriMatcher.addURI(MediaStore.AUTHORITY_LEGACY, "*/audio/playlists/#/members", 175 AUDIO_PLAYLISTS_ID_MEMBERS); basicUriMatcher.addURI(MediaStore.AUTHORITY_LEGACY, "*/file/#", FILES_ID)176 basicUriMatcher.addURI(MediaStore.AUTHORITY_LEGACY, "*/file/#", FILES_ID); 177 }; 178 getAppendedSelection(String selection, Uri uri)179 private static String getAppendedSelection(String selection, Uri uri) { 180 String whereClause = ""; 181 final int match = BASIC_URI_MATCHER.match(uri); 182 switch (match) { 183 case AUDIO_PLAYLISTS_ID_MEMBERS: 184 whereClause = "playlist_id=" + uri.getPathSegments().get(3); 185 break; 186 case FILES_ID: 187 whereClause = "_id=" + uri.getPathSegments().get(2); 188 break; 189 default: 190 // No additional whereClause required 191 } 192 if (selection == null || selection.isEmpty()) { 193 return whereClause; 194 } else if (whereClause.isEmpty()) { 195 return selection; 196 } else { 197 return whereClause + " AND " + selection; 198 } 199 } 200 getTableName(Uri uri)201 private static String getTableName(Uri uri) { 202 final int playlistMatch = BASIC_URI_MATCHER.match(uri); 203 if (playlistMatch == AUDIO_PLAYLISTS_ID_MEMBERS) { 204 return "audio_playlists_map"; 205 } else { 206 // Return the "files" table by default for all other Uris. 207 return "files"; 208 } 209 } 210 211 @Override call(String authority, String method, String arg, Bundle extras)212 public Bundle call(String authority, String method, String arg, Bundle extras) { 213 switch (method) { 214 case START_LEGACY_MIGRATION_CALL: { 215 // Nice to know, but nothing actionable 216 break; 217 } 218 case FINISH_LEGACY_MIGRATION_CALL: { 219 // We're only going to hear this once, since we've either 220 // successfully migrated legacy data, or we're never going to 221 // try again, so it's time to clean things up 222 final String volumeName = arg; 223 switch (volumeName) { 224 case MediaStore.VOLUME_INTERNAL: { 225 mInternalDatabase.close(); 226 getContext().deleteDatabase(INTERNAL_DATABASE_NAME); 227 break; 228 } 229 default: { 230 mExternalDatabase.close(); 231 getContext().deleteDatabase(EXTERNAL_DATABASE_NAME); 232 break; 233 } 234 } 235 } 236 } 237 return null; 238 } 239 240 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)241 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 242 Logging.dumpPersistent(writer); 243 } 244 } 245