1 /* 2 * Copyright (C) 2022 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.util; 18 19 import static android.provider.CloudMediaProviderContract.MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION; 20 import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO; 21 import static android.provider.MediaStore.EXTRA_ALBUM_AUTHORITY; 22 import static android.provider.MediaStore.EXTRA_ALBUM_ID; 23 import static android.provider.MediaStore.EXTRA_CLOUD_PROVIDER; 24 import static android.provider.MediaStore.EXTRA_LOCAL_ONLY; 25 import static android.provider.MediaStore.GET_CLOUD_PROVIDER_CALL; 26 import static android.provider.MediaStore.GET_CLOUD_PROVIDER_RESULT; 27 import static android.provider.MediaStore.PICKER_MEDIA_INIT_CALL; 28 import static android.provider.MediaStore.SET_CLOUD_PROVIDER_CALL; 29 import static android.provider.MediaStore.SET_CLOUD_PROVIDER_RESULT; 30 31 import static com.android.providers.media.PickerUriResolver.getMediaCollectionInfoUri; 32 33 import static java.util.Collections.emptyList; 34 import static java.util.concurrent.TimeUnit.MILLISECONDS; 35 36 import android.annotation.DurationMillisLong; 37 import android.content.ContentProviderClient; 38 import android.content.ContentResolver; 39 import android.content.Context; 40 import android.content.Intent; 41 import android.content.pm.PackageManager; 42 import android.content.pm.ProviderInfo; 43 import android.content.pm.ResolveInfo; 44 import android.os.Bundle; 45 import android.os.Process; 46 import android.os.RemoteException; 47 import android.os.UserHandle; 48 import android.provider.CloudMediaProvider; 49 import android.provider.CloudMediaProviderContract; 50 import android.util.Log; 51 52 import androidx.annotation.NonNull; 53 import androidx.annotation.Nullable; 54 55 import com.android.providers.media.ConfigStore; 56 import com.android.providers.media.photopicker.data.CloudProviderInfo; 57 import com.android.providers.media.photopicker.data.model.UserId; 58 59 import java.util.ArrayList; 60 import java.util.List; 61 import java.util.Objects; 62 import java.util.concurrent.CompletableFuture; 63 import java.util.concurrent.ExecutionException; 64 import java.util.concurrent.TimeoutException; 65 66 /** 67 * Utility methods for retrieving available and/or allowlisted Cloud Providers. 68 * 69 * @see CloudMediaProvider 70 */ 71 public class CloudProviderUtils { 72 private static final String TAG = "CloudProviderUtils"; 73 /** 74 * @return list of available <b>and</b> allowlisted {@link CloudMediaProvider}-s for the current 75 * user. 76 */ getAvailableCloudProviders( @onNull Context context, @NonNull ConfigStore configStore)77 public static List<CloudProviderInfo> getAvailableCloudProviders( 78 @NonNull Context context, @NonNull ConfigStore configStore) { 79 return getAvailableCloudProviders( 80 context, configStore, Process.myUserHandle()); 81 } 82 83 /** 84 * @return list of available <b>and</b> allowlisted {@link CloudMediaProvider}-s for the given 85 * userId. 86 */ getAvailableCloudProviders( @onNull Context context, @NonNull ConfigStore configStore, @NonNull UserHandle userHandle)87 public static List<CloudProviderInfo> getAvailableCloudProviders( 88 @NonNull Context context, @NonNull ConfigStore configStore, 89 @NonNull UserHandle userHandle) { 90 return getAvailableCloudProvidersInternal( 91 context, configStore, /* ignoreAllowList */ false, userHandle); 92 } 93 94 /** 95 * @return list of <b>all</b> available {@link CloudMediaProvider}-s (<b>ignoring</b> the 96 * allowlist) for the current user. 97 */ getAllAvailableCloudProviders( @onNull Context context, @NonNull ConfigStore configStore)98 public static List<CloudProviderInfo> getAllAvailableCloudProviders( 99 @NonNull Context context, @NonNull ConfigStore configStore) { 100 return getAllAvailableCloudProviders(context, configStore, Process.myUserHandle()); 101 } 102 103 /** 104 * @return list of <b>all</b> available {@link CloudMediaProvider}-s (<b>ignoring</b> the 105 * allowlist) for the given userId. 106 */ getAllAvailableCloudProviders( @onNull Context context, @NonNull ConfigStore configStore, @NonNull UserHandle userHandle)107 public static List<CloudProviderInfo> getAllAvailableCloudProviders( 108 @NonNull Context context, @NonNull ConfigStore configStore, 109 @NonNull UserHandle userHandle) { 110 return getAvailableCloudProvidersInternal(context, configStore, /* ignoreAllowList */ true, 111 userHandle); 112 } 113 getAvailableCloudProvidersInternal( @onNull Context context, @NonNull ConfigStore configStore, boolean ignoreAllowlist, @NonNull UserHandle userHandle)114 private static List<CloudProviderInfo> getAvailableCloudProvidersInternal( 115 @NonNull Context context, @NonNull ConfigStore configStore, boolean ignoreAllowlist, 116 @NonNull UserHandle userHandle) { 117 if (!configStore.isCloudMediaInPhotoPickerEnabled()) { 118 Log.i(TAG, "Returning an empty list of available cloud providers since the " 119 + "Cloud-Media-in-Photo-Picker feature is disabled."); 120 return emptyList(); 121 } 122 123 Objects.requireNonNull(context); 124 125 ignoreAllowlist = ignoreAllowlist || !configStore.shouldEnforceCloudProviderAllowlist(); 126 127 final List<CloudProviderInfo> providers = new ArrayList<>(); 128 129 // We do not need to get the allowlist from the ConfigStore if we are going to skip 130 // if-allowlisted check below. 131 final List<String> allowlistedPackages = 132 ignoreAllowlist ? null : configStore.getAllowedCloudProviderPackages(); 133 134 final Intent intent = new Intent(CloudMediaProviderContract.PROVIDER_INTERFACE); 135 final List<ResolveInfo> allAvailableProviders = getAllCloudProvidersForUser(context, 136 intent, userHandle); 137 138 for (ResolveInfo info : allAvailableProviders) { 139 final ProviderInfo providerInfo = info.providerInfo; 140 if (providerInfo.authority == null) { 141 // Provider does NOT declare an authority. 142 continue; 143 } 144 145 if (!MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION.equals(providerInfo.readPermission)) { 146 // Provider does NOT have the right read permission. 147 continue; 148 } 149 150 if (!ignoreAllowlist && !allowlistedPackages.contains(providerInfo.packageName)) { 151 // Provider is not allowlisted. 152 continue; 153 } 154 155 final CloudProviderInfo cloudProvider = new CloudProviderInfo( 156 providerInfo.authority, 157 providerInfo.applicationInfo.packageName, 158 providerInfo.applicationInfo.uid); 159 providers.add(cloudProvider); 160 } 161 162 Log.d(TAG, (ignoreAllowlist ? "All (ignoring allowlist)" : "") 163 + "Available CloudMediaProvider-s: " + providers); 164 return providers; 165 } 166 167 /** 168 * Returns a list of all available providers with the given intent for a userId. If userId is 169 * null, results are returned for the current user. 170 */ getAllCloudProvidersForUser(@onNull Context context, @NonNull Intent intent, @NonNull UserHandle userHandle)171 private static List<ResolveInfo> getAllCloudProvidersForUser(@NonNull Context context, 172 @NonNull Intent intent, @NonNull UserHandle userHandle) { 173 return context.getPackageManager() 174 .queryIntentContentProvidersAsUser(intent, 0, userHandle); 175 } 176 177 /** 178 * Request content provider to change cloud provider. 179 */ persistSelectedProvider( @onNull ContentProviderClient client, @Nullable String newCloudProvider)180 public static boolean persistSelectedProvider( 181 @NonNull ContentProviderClient client, 182 @Nullable String newCloudProvider) throws RemoteException { 183 final Bundle input = new Bundle(); 184 input.putString(EXTRA_CLOUD_PROVIDER, newCloudProvider); 185 final Bundle result = 186 client.call(SET_CLOUD_PROVIDER_CALL, /* arg */ null, /* extras */ input); 187 return result.getBoolean(SET_CLOUD_PROVIDER_RESULT, /* defaultValue= */ false); 188 } 189 190 /** 191 * Fetch selected cloud provider from content provider. 192 * @return fetched cloud provider authority. 193 */ 194 @Nullable fetchProviderAuthority( @onNull ContentProviderClient client)195 public static String fetchProviderAuthority( 196 @NonNull ContentProviderClient client) throws RemoteException { 197 final Bundle result = client.call(GET_CLOUD_PROVIDER_CALL, /* arg */ null, 198 /* extras */ null); 199 return result.getString(GET_CLOUD_PROVIDER_RESULT); 200 } 201 202 /** 203 * Send data init call. 204 */ sendInitPhotoPickerDataNotification( @onNull ContentProviderClient client, @Nullable String albumId, @Nullable String albumAuthority, boolean initLocalOnlyData)205 public static boolean sendInitPhotoPickerDataNotification( 206 @NonNull ContentProviderClient client, 207 @Nullable String albumId, 208 @Nullable String albumAuthority, 209 boolean initLocalOnlyData) throws RemoteException { 210 final Bundle input = new Bundle(); 211 input.putString(EXTRA_ALBUM_ID, albumId); 212 input.putString(EXTRA_ALBUM_AUTHORITY, albumAuthority); 213 input.putBoolean(EXTRA_LOCAL_ONLY, initLocalOnlyData); 214 Log.i(TAG, "Sending media init query for extras: " + input); 215 216 client.call(PICKER_MEDIA_INIT_CALL, /* arg */ null, /* extras */ input); 217 return true; 218 } 219 220 /** 221 * @return the label for the {@link ProviderInfo} with {@code authority} for the given 222 * {@link UserHandle}. 223 */ 224 @Nullable getProviderLabelForUser(@onNull Context context, @NonNull UserHandle user, @Nullable String authority)225 public static String getProviderLabelForUser(@NonNull Context context, @NonNull UserHandle user, 226 @Nullable String authority) throws PackageManager.NameNotFoundException { 227 if (authority == null) { 228 return null; 229 } 230 231 final PackageManager packageManager = UserId.of(user).getPackageManager(context); 232 return getProviderLabel(packageManager, authority); 233 } 234 235 /** 236 * @return the label for the {@link ProviderInfo} with {@code authority}. 237 */ 238 @NonNull getProviderLabel(@onNull PackageManager packageManager, @NonNull String authority)239 public static String getProviderLabel(@NonNull PackageManager packageManager, 240 @NonNull String authority) { 241 final ProviderInfo providerInfo = packageManager.resolveContentProvider( 242 authority, /* flags */ 0); 243 return getProviderLabel(packageManager, providerInfo); 244 } 245 246 /** 247 * @return the label for the given {@link ProviderInfo}. 248 */ 249 @NonNull getProviderLabel(@onNull PackageManager packageManager, @NonNull ProviderInfo providerInfo)250 public static String getProviderLabel(@NonNull PackageManager packageManager, 251 @NonNull ProviderInfo providerInfo) { 252 return String.valueOf(providerInfo.loadLabel(packageManager)); 253 } 254 255 /** 256 * @param resolver {@link ContentResolver} for the related user 257 * @param cloudMediaProviderAuthority authority {@link String} of the {@link CloudMediaProvider} 258 * @param timeout timeout in milliseconds for this query (<= 0 for timeout) 259 * @return the current cloud media collection info for the {@link CloudMediaProvider} with the 260 * given {@code cloudMediaProviderAuthority}. 261 */ 262 @Nullable getCloudMediaCollectionInfo(@onNull ContentResolver resolver, @Nullable String cloudMediaProviderAuthority, @DurationMillisLong long timeout)263 public static Bundle getCloudMediaCollectionInfo(@NonNull ContentResolver resolver, 264 @Nullable String cloudMediaProviderAuthority, @DurationMillisLong long timeout) 265 throws ExecutionException, InterruptedException, TimeoutException { 266 if (cloudMediaProviderAuthority == null) { 267 return null; 268 } 269 270 CompletableFuture<Bundle> future = CompletableFuture.supplyAsync(() -> 271 resolver.call(getMediaCollectionInfoUri(cloudMediaProviderAuthority), 272 METHOD_GET_MEDIA_COLLECTION_INFO, /* arg */ null, 273 /* extras */ new Bundle())); 274 275 return (timeout > 0) ? future.get(timeout, MILLISECONDS) : future.get(); 276 } 277 } 278