• 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;
18 
19 import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO;
20 
21 import static com.android.providers.media.PickerUriResolver.getAlbumUri;
22 import static com.android.providers.media.PickerUriResolver.getMediaCollectionInfoUri;
23 
24 import android.content.Context;
25 import android.content.Intent;
26 import android.database.Cursor;
27 import android.database.MergeCursor;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.provider.CloudMediaProviderContract;
31 import android.provider.MediaStore;
32 import android.text.TextUtils;
33 import android.util.Log;
34 
35 import com.android.providers.media.photopicker.data.CloudProviderQueryExtras;
36 import com.android.providers.media.photopicker.data.PickerDbFacade;
37 
38 import java.util.ArrayList;
39 import java.util.List;
40 
41 /**
42  * Fetches data for the picker UI from the db and cloud/local providers
43  */
44 public class PickerDataLayer {
45     private static final String TAG = "PickerDataLayer";
46 
47     private final Context mContext;
48     private final PickerDbFacade mDbFacade;
49     private final PickerSyncController mSyncController;
50     private final String mLocalProvider;
51 
PickerDataLayer(Context context, PickerDbFacade dbFacade, PickerSyncController syncController)52     public PickerDataLayer(Context context, PickerDbFacade dbFacade,
53             PickerSyncController syncController) {
54         mContext = context;
55         mDbFacade = dbFacade;
56         mSyncController = syncController;
57         mLocalProvider = dbFacade.getLocalProvider();
58     }
59 
fetchMedia(Bundle queryArgs)60     public Cursor fetchMedia(Bundle queryArgs) {
61         final CloudProviderQueryExtras queryExtras
62                 = CloudProviderQueryExtras.fromMediaStoreBundle(queryArgs, mLocalProvider);
63         final String albumId = queryExtras.getAlbumId();
64         final String authority = queryExtras.getAlbumAuthority();
65         // Use media table for all media except albums. Merged categories like,
66         // favorites and video are tagged in the media table and are not a part of
67         // album_media.
68         if (TextUtils.isEmpty(albumId) || isMergedAlbum(queryExtras)) {
69             // Refresh the 'media' table
70             mSyncController.syncAllMedia();
71 
72             if (TextUtils.isEmpty(albumId)) {
73                 // Notify that the picker is launched in case there's any pending UI notification
74                 mSyncController.notifyPickerLaunch();
75             }
76 
77             // Fetch all merged and deduped cloud and local media from 'media' table
78             // This also matches 'merged' albums like Favorites because |authority| will
79             // be null, hence we have to fetch the data from the picker db
80             return mDbFacade.queryMediaForUi(queryExtras.toQueryFilter());
81         } else {
82             // The album type here can only be local or cloud because merged categories like,
83             // Favorites and Videos would hit the first condition.
84             // Refresh the 'album_media' table
85             mSyncController.syncAlbumMedia(albumId, isLocal(authority));
86 
87             // Fetch album specific media for local or cloud from 'album_media' table
88             return mDbFacade.queryAlbumMediaForUi(queryExtras.toQueryFilter(), authority);
89         }
90     }
91 
92     /**
93      * Checks if the query is for a merged album type.
94      * Some albums are not cloud only, they are merged from files on devices and the cloudprovider.
95      */
isMergedAlbum(CloudProviderQueryExtras queryExtras)96     private boolean isMergedAlbum(CloudProviderQueryExtras queryExtras) {
97         final boolean isFavorite = queryExtras.isFavorite();
98         final boolean isVideo = queryExtras.isVideo();
99         return isFavorite || isVideo;
100     }
101 
fetchAlbums(Bundle queryArgs)102     public Cursor fetchAlbums(Bundle queryArgs) {
103         // Refresh the 'media' table so that 'merged' albums (Favorites and Videos) are up to date
104         mSyncController.syncAllMedia();
105 
106         final String cloudProvider = mDbFacade.getCloudProvider();
107         final CloudProviderQueryExtras queryExtras
108                 = CloudProviderQueryExtras.fromMediaStoreBundle(queryArgs, mLocalProvider);
109         final Bundle cloudMediaArgs = queryExtras.toCloudMediaBundle();
110         final List<Cursor> cursors = new ArrayList<>();
111         final Bundle cursorExtra = new Bundle();
112         cursorExtra.putString(MediaStore.EXTRA_CLOUD_PROVIDER, cloudProvider);
113 
114         // Favorites and Videos are merged albums.
115         final Cursor mergedAlbums = mDbFacade.getMergedAlbums(queryExtras.toQueryFilter());
116         if (mergedAlbums != null) {
117             cursors.add(mergedAlbums);
118         }
119 
120         final Cursor localAlbums = queryProviderAlbums(mLocalProvider, cloudMediaArgs);
121         if (localAlbums != null) {
122             cursors.add(localAlbums);
123         }
124 
125         final Cursor cloudAlbums = queryProviderAlbums(cloudProvider, cloudMediaArgs);
126         if (cloudAlbums != null) {
127             cursors.add(cloudAlbums);
128         }
129 
130         if (cursors.isEmpty()) {
131             return null;
132         }
133 
134         MergeCursor mergeCursor = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
135         mergeCursor.setExtras(cursorExtra);
136         return mergeCursor;
137     }
138 
fetchCloudAccountInfo()139     public AccountInfo fetchCloudAccountInfo() {
140         final String cloudProvider = mDbFacade.getCloudProvider();
141         if (cloudProvider == null) {
142             return null;
143         }
144 
145         try {
146             final Bundle accountBundle = mContext.getContentResolver().call(
147                     getMediaCollectionInfoUri(cloudProvider), METHOD_GET_MEDIA_COLLECTION_INFO,
148                     /* arg */ null, /* extras */ null);
149             final String accountName = accountBundle.getString(
150                     CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME);
151             final Intent configIntent = (Intent) accountBundle.getParcelable(
152                     CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_CONFIGURATION_INTENT);
153 
154             if (accountName == null) {
155                 return null;
156             }
157 
158             return new AccountInfo(accountName, configIntent);
159         } catch (Exception e) {
160             Log.w(TAG, "Failed to fetch account info from cloud provider: " + cloudProvider, e);
161             return null;
162         }
163     }
164 
queryProviderAlbums(String authority, Bundle queryArgs)165     private Cursor queryProviderAlbums(String authority, Bundle queryArgs) {
166         if (authority == null) {
167             // Can happen if there is no cloud provider
168             return null;
169         }
170 
171         return query(getAlbumUri(authority), queryArgs);
172     }
173 
query(Uri uri, Bundle extras)174     private Cursor query(Uri uri, Bundle extras) {
175         return mContext.getContentResolver().query(uri, /* projection */ null, extras,
176                 /* cancellationSignal */ null);
177     }
178 
isLocal(String authority)179     private boolean isLocal(String authority) {
180         return mLocalProvider.equals(authority);
181     }
182 
183     public static class AccountInfo {
184         public final String accountName;
185         public final Intent accountConfigurationIntent;
186 
AccountInfo(String accountName, Intent accountConfigurationIntent)187         public AccountInfo(String accountName, Intent accountConfigurationIntent) {
188             this.accountName = accountName;
189             this.accountConfigurationIntent = accountConfigurationIntent;
190         }
191     }
192 }
193