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.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 DatabaseHelper mInternalDatabase; 56 private DatabaseHelper 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 DatabaseHelper(context, INTERNAL_DATABASE_NAME, 83 true, false, true, null, null, null, null, null); 84 mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, 85 false, false, true, null, null, null, null, null); 86 87 return true; 88 } 89 getDatabaseForUri(Uri uri)90 private @NonNull DatabaseHelper getDatabaseForUri(Uri uri) { 91 final String volumeName = MediaStore.getVolumeName(uri); 92 switch (volumeName) { 93 case MediaStore.VOLUME_INTERNAL: 94 return Objects.requireNonNull(mInternalDatabase, "Missing internal database"); 95 default: 96 return Objects.requireNonNull(mExternalDatabase, "Missing external database"); 97 } 98 } 99 100 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)101 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 102 String sortOrder) { 103 final String appendedSelection = getAppendedSelection(selection, uri); 104 final DatabaseHelper helper = getDatabaseForUri(uri); 105 return helper.runWithoutTransaction((db) -> { 106 return db.query(getTableName(uri), projection, appendedSelection, selectionArgs, 107 null, null, sortOrder); 108 }); 109 } 110 111 @Override getType(Uri uri)112 public String getType(Uri uri) { 113 throw new UnsupportedOperationException(); 114 } 115 116 @Override applyBatch(ArrayList<ContentProviderOperation> operations)117 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 118 throws OperationApplicationException { 119 // Open transactions on databases for requested volumes 120 final Set<DatabaseHelper> transactions = new ArraySet<>(); 121 try { 122 for (ContentProviderOperation op : operations) { 123 final DatabaseHelper helper = getDatabaseForUri(op.getUri()); 124 if (!transactions.contains(helper)) { 125 helper.beginTransaction(); 126 transactions.add(helper); 127 } 128 } 129 130 final ContentProviderResult[] result = super.applyBatch(operations); 131 for (DatabaseHelper helper : transactions) { 132 helper.setTransactionSuccessful(); 133 } 134 return result; 135 } finally { 136 for (DatabaseHelper helper : transactions) { 137 helper.endTransaction(); 138 } 139 } 140 } 141 142 @Override insert(Uri uri, ContentValues values)143 public Uri insert(Uri uri, ContentValues values) { 144 if (!uri.getBooleanQueryParameter("silent", false)) { 145 try { 146 final File file = new File(values.getAsString(MediaColumns.DATA)); 147 file.getParentFile().mkdirs(); 148 file.createNewFile(); 149 } catch (IOException e) { 150 throw new IllegalStateException(e); 151 } 152 } 153 154 final DatabaseHelper helper = getDatabaseForUri(uri); 155 final long id = helper.runWithTransaction((db) -> { 156 return db.insert(getTableName(uri), null, values); 157 }); 158 return ContentUris.withAppendedId(uri, id); 159 } 160 161 @Override delete(Uri uri, String selection, String[] selectionArgs)162 public int delete(Uri uri, String selection, String[] selectionArgs) { 163 throw new UnsupportedOperationException(); 164 } 165 166 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)167 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 168 throw new UnsupportedOperationException(); 169 } 170 171 private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112; 172 private static final int FILES_ID = 701; 173 private static final UriMatcher BASIC_URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); 174 static { 175 final UriMatcher basicUriMatcher = BASIC_URI_MATCHER; basicUriMatcher.addURI(MediaStore.AUTHORITY_LEGACY, "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS)176 basicUriMatcher.addURI(MediaStore.AUTHORITY_LEGACY, "*/audio/playlists/#/members", 177 AUDIO_PLAYLISTS_ID_MEMBERS); basicUriMatcher.addURI(MediaStore.AUTHORITY_LEGACY, "*/file/#", FILES_ID)178 basicUriMatcher.addURI(MediaStore.AUTHORITY_LEGACY, "*/file/#", FILES_ID); 179 }; 180 getAppendedSelection(String selection, Uri uri)181 private static String getAppendedSelection(String selection, Uri uri) { 182 String whereClause = ""; 183 final int match = BASIC_URI_MATCHER.match(uri); 184 switch (match) { 185 case AUDIO_PLAYLISTS_ID_MEMBERS: 186 whereClause = "playlist_id=" + uri.getPathSegments().get(3); 187 break; 188 case FILES_ID: 189 whereClause = "_id=" + uri.getPathSegments().get(2); 190 break; 191 default: 192 // No additional whereClause required 193 } 194 if (selection == null || selection.isEmpty()) { 195 return whereClause; 196 } else if (whereClause.isEmpty()) { 197 return selection; 198 } else { 199 return whereClause + " AND " + selection; 200 } 201 } 202 getTableName(Uri uri)203 private static String getTableName(Uri uri) { 204 final int playlistMatch = BASIC_URI_MATCHER.match(uri); 205 if (playlistMatch == AUDIO_PLAYLISTS_ID_MEMBERS) { 206 return "audio_playlists_map"; 207 } else { 208 // Return the "files" table by default for all other Uris. 209 return "files"; 210 } 211 } 212 213 @Override call(String authority, String method, String arg, Bundle extras)214 public Bundle call(String authority, String method, String arg, Bundle extras) { 215 switch (method) { 216 case START_LEGACY_MIGRATION_CALL: { 217 // Nice to know, but nothing actionable 218 break; 219 } 220 case FINISH_LEGACY_MIGRATION_CALL: { 221 // We're only going to hear this once, since we've either 222 // successfully migrated legacy data, or we're never going to 223 // try again, so it's time to clean things up 224 final String volumeName = arg; 225 switch (volumeName) { 226 case MediaStore.VOLUME_INTERNAL: { 227 mInternalDatabase.close(); 228 getContext().deleteDatabase(INTERNAL_DATABASE_NAME); 229 break; 230 } 231 default: { 232 mExternalDatabase.close(); 233 getContext().deleteDatabase(EXTERNAL_DATABASE_NAME); 234 break; 235 } 236 } 237 } 238 } 239 return null; 240 } 241 242 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)243 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 244 Logging.dumpPersistent(writer); 245 } 246 } 247