• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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