1 /* 2 * Copyright (C) 2021 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.photopicker.data; 18 19 import static com.android.providers.media.photopicker.util.CursorUtils.getCursorString; 20 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.ContentProvider; 24 import android.content.ContentProviderClient; 25 import android.content.ContentResolver; 26 import android.content.Context; 27 import android.content.pm.PackageManager.NameNotFoundException; 28 import android.database.Cursor; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.os.RemoteException; 32 import android.os.UserHandle; 33 import android.provider.CloudMediaProviderContract; 34 import android.provider.CloudMediaProviderContract.AlbumColumns; 35 import android.provider.MediaStore; 36 import android.text.TextUtils; 37 import android.util.Log; 38 39 import com.android.modules.utils.build.SdkLevel; 40 import com.android.providers.media.PickerUriResolver; 41 import com.android.providers.media.photopicker.data.PickerDbFacade; 42 import com.android.providers.media.photopicker.data.model.Category; 43 import com.android.providers.media.photopicker.data.model.UserId; 44 45 /** 46 * Provides image and video items from {@link MediaStore} collection to the Photo Picker. 47 */ 48 public class ItemsProvider { 49 50 private static final String TAG = ItemsProvider.class.getSimpleName(); 51 52 private final Context mContext; 53 ItemsProvider(Context context)54 public ItemsProvider(Context context) { 55 mContext = context; 56 } 57 58 /** 59 * Returns a {@link Cursor} to all images/videos based on the param passed for 60 * {@code categoryType}, {@code offset}, {@code limit}, {@code mimeType} and {@code userId}. 61 * 62 * <p> 63 * By default the returned {@link Cursor} sorts by latest date taken. 64 * 65 * @param category the category of items to return. May be cloud, local or merged albums like 66 * favorites or videos. 67 * @param offset the offset after which to return items. 68 * @param limit the limit of number of items to return. 69 * @param mimeType the mime type of item. {@code null} returns all images/videos that are 70 * scanned by {@link MediaStore}. 71 * @param userId the {@link UserId} of the user to get items as. 72 * {@code null} defaults to {@link UserId#CURRENT_USER} 73 * 74 * @return {@link Cursor} to images/videos on external storage that are scanned by 75 * {@link MediaStore}. The returned cursor is filtered based on params passed, it {@code null} 76 * if there are no such images/videos. The Cursor for each item contains {@link ItemColumns} 77 */ 78 @Nullable getItems(Category category, int offset, int limit, @Nullable String mimeType, @Nullable UserId userId)79 public Cursor getItems(Category category, int offset, 80 int limit, @Nullable String mimeType, @Nullable UserId userId) throws 81 IllegalArgumentException { 82 if (userId == null) { 83 userId = UserId.CURRENT_USER; 84 } 85 86 return queryMedia(limit, mimeType, category, userId); 87 } 88 89 /** 90 * Returns a {@link Cursor} to all non-empty categories in which images/videos are categorised. 91 * This includes: 92 * * A constant list of local categories for on-device images/videos: {@link Category} 93 * * Albums provided by selected cloud provider 94 * 95 * @param mimeType the mime type of item. {@code null} returns all images/videos that are 96 * scanned by {@link MediaStore}. 97 * @param userId the {@link UserId} of the user to get categories as. 98 * {@code null} defaults to {@link UserId#CURRENT_USER}. 99 * 100 * @return {@link Cursor} for each category would contain the following columns in 101 * their relative order: 102 * categoryName: {@link CategoryColumns#NAME} The name of the category, 103 * categoryCoverId: {@link CategoryColumns#COVER_ID} The id for the cover of 104 * the category. By default this will be the most recent image/video in that 105 * category, 106 * categoryNumberOfItems: {@link CategoryColumns#NUMBER_OF_ITEMS} number of image/video items 107 * in the category, 108 */ 109 @Nullable getCategories(@ullable String mimeType, @Nullable UserId userId)110 public Cursor getCategories(@Nullable String mimeType, @Nullable UserId userId) { 111 if (userId == null) { 112 userId = UserId.CURRENT_USER; 113 } 114 115 return queryAlbums(mimeType, userId); 116 } 117 queryMedia(int limit, @Nullable String mimeType, @NonNull Category category, @NonNull UserId userId)118 private Cursor queryMedia(int limit, @Nullable String mimeType, 119 @NonNull Category category, @NonNull UserId userId) 120 throws IllegalStateException { 121 final Bundle extras = new Bundle(); 122 try (ContentProviderClient client = userId.getContentResolver(mContext) 123 .acquireUnstableContentProviderClient(MediaStore.AUTHORITY)) { 124 if (client == null) { 125 Log.e(TAG, "Unable to acquire unstable content provider for " 126 + MediaStore.AUTHORITY); 127 return null; 128 } 129 extras.putInt(MediaStore.QUERY_ARG_LIMIT, limit); 130 extras.putString(MediaStore.QUERY_ARG_MIME_TYPE, mimeType); 131 extras.putString(MediaStore.QUERY_ARG_ALBUM_ID, category.getId()); 132 extras.putString(MediaStore.QUERY_ARG_ALBUM_AUTHORITY, category.getAuthority()); 133 134 final Uri uri = PickerUriResolver.PICKER_INTERNAL_URI.buildUpon() 135 .appendPath(PickerUriResolver.MEDIA_PATH).build(); 136 137 return client.query(uri, /* projection */ null, extras, /* cancellationSignal */ null); 138 } catch (RemoteException | NameNotFoundException ignored) { 139 // Do nothing, return null. 140 Log.e(TAG, "Failed to query merged media with extras: " 141 + extras + ". userId = " + userId, ignored); 142 return null; 143 } 144 } 145 146 @Nullable queryAlbums(@ullable String mimeType, @NonNull UserId userId)147 private Cursor queryAlbums(@Nullable String mimeType, @NonNull UserId userId) { 148 final Bundle extras = new Bundle(); 149 try (ContentProviderClient client = userId.getContentResolver(mContext) 150 .acquireUnstableContentProviderClient(MediaStore.AUTHORITY)) { 151 if (client == null) { 152 Log.e(TAG, "Unable to acquire unstable content provider for " 153 + MediaStore.AUTHORITY); 154 return null; 155 } 156 extras.putString(MediaStore.QUERY_ARG_MIME_TYPE, mimeType); 157 158 final Uri uri = PickerUriResolver.PICKER_INTERNAL_URI.buildUpon() 159 .appendPath(PickerUriResolver.ALBUM_PATH).build(); 160 161 return client.query(uri, /* projection */ null, extras, /* cancellationSignal */ null); 162 } catch (RemoteException | NameNotFoundException ignored) { 163 // Do nothing, return null. 164 Log.w(TAG, "Failed to query merged albums with extras: " 165 + extras + ". userId = " + userId, ignored); 166 return null; 167 } 168 } 169 getItemsUri(String id, String authority, UserId userId)170 public static Uri getItemsUri(String id, String authority, UserId userId) { 171 final Uri uri = PickerUriResolver.getMediaUri(authority).buildUpon() 172 .appendPath(id).build(); 173 174 if (userId.equals(UserId.CURRENT_USER)) { 175 return uri; 176 } 177 178 return createContentUriForUser(uri, userId.getUserHandle()); 179 } 180 createContentUriForUser(Uri uri, UserHandle userHandle)181 private static Uri createContentUriForUser(Uri uri, UserHandle userHandle) { 182 if (SdkLevel.isAtLeastS()) { 183 return ContentProvider.createContentUriForUser(uri, userHandle); 184 } 185 186 return createContentUriForUserImpl(uri, userHandle); 187 } 188 189 /** 190 * This method is a copy of {@link ContentProvider#createContentUriForUser(Uri, UserHandle)} 191 * which is a System API added in Android S. 192 */ createContentUriForUserImpl(Uri uri, UserHandle userHandle)193 private static Uri createContentUriForUserImpl(Uri uri, UserHandle userHandle) { 194 if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { 195 throw new IllegalArgumentException(String.format( 196 "Given URI [%s] is not a content URI: ", uri)); 197 } 198 199 int userId = userHandle.getIdentifier(); 200 if (uriHasUserId(uri)) { 201 if (String.valueOf(userId).equals(uri.getUserInfo())) { 202 return uri; 203 } 204 throw new IllegalArgumentException(String.format( 205 "Given URI [%s] already has a user ID, different from given user handle [%s]", 206 uri, 207 userId)); 208 } 209 210 Uri.Builder builder = uri.buildUpon(); 211 builder.encodedAuthority( 212 "" + userHandle.getIdentifier() + "@" + uri.getEncodedAuthority()); 213 return builder.build(); 214 } 215 uriHasUserId(Uri uri)216 private static boolean uriHasUserId(Uri uri) { 217 if (uri == null) return false; 218 return !TextUtils.isEmpty(uri.getUserInfo()); 219 } 220 } 221