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