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