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