blob: dbddcebc148871573f6368ef3494e82d1b8b0b6f [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.providers.media.photopicker.util;
import static android.provider.CloudMediaProviderContract.MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION;
import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_COLLECTION_INFO;
import static android.provider.CloudMediaProviderContract.MediaCollectionInfo.ACCOUNT_NAME;
import static android.provider.MediaStore.EXTRA_ALBUM_AUTHORITY;
import static android.provider.MediaStore.EXTRA_ALBUM_ID;
import static android.provider.MediaStore.EXTRA_CLOUD_PROVIDER;
import static android.provider.MediaStore.EXTRA_LOCAL_ONLY;
import static android.provider.MediaStore.GET_CLOUD_PROVIDER_CALL;
import static android.provider.MediaStore.GET_CLOUD_PROVIDER_RESULT;
import static android.provider.MediaStore.PICKER_MEDIA_INIT_CALL;
import static android.provider.MediaStore.SET_CLOUD_PROVIDER_CALL;
import static com.android.providers.media.PickerUriResolver.getMediaCollectionInfoUri;
import static java.util.Collections.emptyList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.annotation.DurationMillisLong;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.CloudMediaProvider;
import android.provider.CloudMediaProviderContract;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.providers.media.ConfigStore;
import com.android.providers.media.photopicker.data.CloudProviderInfo;
import com.android.providers.media.photopicker.data.model.UserId;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
/**
* Utility methods for retrieving available and/or allowlisted Cloud Providers.
*
* @see CloudMediaProvider
*/
public class CloudProviderUtils {
private static final String TAG = "CloudProviderUtils";
/**
* @return list of available <b>and</b> allowlisted {@link CloudMediaProvider}-s for the current
* user.
*/
public static List<CloudProviderInfo> getAvailableCloudProviders(
@NonNull Context context, @NonNull ConfigStore configStore) {
return getAvailableCloudProviders(
context, configStore, Process.myUserHandle());
}
/**
* @return list of available <b>and</b> allowlisted {@link CloudMediaProvider}-s for the given
* userId.
*/
public static List<CloudProviderInfo> getAvailableCloudProviders(
@NonNull Context context, @NonNull ConfigStore configStore,
@NonNull UserHandle userHandle) {
return getAvailableCloudProvidersInternal(
context, configStore, /* ignoreAllowList */ false, userHandle);
}
/**
* @return list of <b>all</b> available {@link CloudMediaProvider}-s (<b>ignoring</b> the
* allowlist) for the current user.
*/
public static List<CloudProviderInfo> getAllAvailableCloudProviders(
@NonNull Context context, @NonNull ConfigStore configStore) {
return getAllAvailableCloudProviders(context, configStore, Process.myUserHandle());
}
/**
* @return list of <b>all</b> available {@link CloudMediaProvider}-s (<b>ignoring</b> the
* allowlist) for the given userId.
*/
public static List<CloudProviderInfo> getAllAvailableCloudProviders(
@NonNull Context context, @NonNull ConfigStore configStore,
@NonNull UserHandle userHandle) {
return getAvailableCloudProvidersInternal(context, configStore, /* ignoreAllowList */ true,
userHandle);
}
private static List<CloudProviderInfo> getAvailableCloudProvidersInternal(
@NonNull Context context, @NonNull ConfigStore configStore, boolean ignoreAllowlist,
@NonNull UserHandle userHandle) {
if (!configStore.isCloudMediaInPhotoPickerEnabled()) {
Log.i(TAG, "Returning an empty list of available cloud providers since the "
+ "Cloud-Media-in-Photo-Picker feature is disabled.");
return emptyList();
}
Objects.requireNonNull(context);
ignoreAllowlist = ignoreAllowlist || !configStore.shouldEnforceCloudProviderAllowlist();
final List<CloudProviderInfo> providers = new ArrayList<>();
// We do not need to get the allowlist from the ConfigStore if we are going to skip
// if-allowlisted check below.
final List<String> allowlistedPackages =
ignoreAllowlist ? null : configStore.getAllowedCloudProviderPackages();
final Intent intent = new Intent(CloudMediaProviderContract.PROVIDER_INTERFACE);
final List<ResolveInfo> allAvailableProviders = getAllCloudProvidersForUser(context,
intent, userHandle);
for (ResolveInfo info : allAvailableProviders) {
final ProviderInfo providerInfo = info.providerInfo;
if (providerInfo.authority == null) {
// Provider does NOT declare an authority.
continue;
}
if (!MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION.equals(providerInfo.readPermission)) {
// Provider does NOT have the right read permission.
continue;
}
if (!ignoreAllowlist && !allowlistedPackages.contains(providerInfo.packageName)) {
// Provider is not allowlisted.
continue;
}
final CloudProviderInfo cloudProvider = new CloudProviderInfo(
providerInfo.authority,
providerInfo.applicationInfo.packageName,
providerInfo.applicationInfo.uid);
providers.add(cloudProvider);
}
Log.d(TAG, (ignoreAllowlist ? "All (ignoring allowlist)" : "")
+ "Available CloudMediaProvider-s: " + providers);
return providers;
}
/**
* Returns a list of all available providers with the given intent for a userId. If userId is
* null, results are returned for the current user.
*/
private static List<ResolveInfo> getAllCloudProvidersForUser(@NonNull Context context,
@NonNull Intent intent, @NonNull UserHandle userHandle) {
return context.getPackageManager()
.queryIntentContentProvidersAsUser(intent, 0, userHandle);
}
/**
* Request content provider to change cloud provider.
*/
public static boolean persistSelectedProvider(
@NonNull ContentProviderClient client,
@Nullable String newCloudProvider) throws RemoteException {
final Bundle input = new Bundle();
input.putString(EXTRA_CLOUD_PROVIDER, newCloudProvider);
client.call(SET_CLOUD_PROVIDER_CALL, /* arg */ null, /* extras */ input);
return true;
}
/**
* Fetch selected cloud provider from content provider.
* @param defaultAuthority is the default returned in case query result is null.
* @return fetched cloud provider authority if it is non-null.
* Otherwise return defaultAuthority.
*/
@Nullable
public static String fetchProviderAuthority(
@NonNull ContentProviderClient client,
@NonNull String defaultAuthority) throws RemoteException {
final Bundle result = client.call(GET_CLOUD_PROVIDER_CALL, /* arg */ null,
/* extras */ null);
return result.getString(GET_CLOUD_PROVIDER_RESULT, defaultAuthority);
}
/**
* Send data init call.
*/
public static boolean sendInitPhotoPickerDataNotification(
@NonNull ContentProviderClient client,
@Nullable String albumId,
@Nullable String albumAuthority,
boolean initLocalOnlyData) throws RemoteException {
final Bundle input = new Bundle();
input.putString(EXTRA_ALBUM_ID, albumId);
input.putString(EXTRA_ALBUM_AUTHORITY, albumAuthority);
input.putBoolean(EXTRA_LOCAL_ONLY, initLocalOnlyData);
Log.i(TAG, "Sending media init query for extras: " + input);
client.call(PICKER_MEDIA_INIT_CALL, /* arg */ null, /* extras */ input);
return true;
}
/**
* @return the label for the {@link ProviderInfo} with {@code authority} for the given
* {@link UserHandle}.
*/
@Nullable
public static String getProviderLabelForUser(@NonNull Context context, @NonNull UserHandle user,
@Nullable String authority) throws PackageManager.NameNotFoundException {
if (authority == null) {
return null;
}
final PackageManager packageManager = UserId.of(user).getPackageManager(context);
return getProviderLabel(packageManager, authority);
}
/**
* @return the label for the {@link ProviderInfo} with {@code authority}.
*/
@NonNull
public static String getProviderLabel(@NonNull PackageManager packageManager,
@NonNull String authority) {
final ProviderInfo providerInfo = packageManager.resolveContentProvider(
authority, /* flags */ 0);
return getProviderLabel(packageManager, providerInfo);
}
/**
* @return the label for the given {@link ProviderInfo}.
*/
@NonNull
public static String getProviderLabel(@NonNull PackageManager packageManager,
@NonNull ProviderInfo providerInfo) {
return String.valueOf(providerInfo.loadLabel(packageManager));
}
/**
* @param resolver {@link ContentResolver} for the related user
* @param cloudMediaProviderAuthority authority {@link String} of the {@link CloudMediaProvider}
* @param timeout timeout in milliseconds for this query (<= 0 for timeout)
* @return the current cloud media account name for the {@link CloudMediaProvider} with the
* given {@code cloudMediaProviderAuthority}.
*/
@Nullable
public static String getCloudMediaAccountName(@NonNull ContentResolver resolver,
@Nullable String cloudMediaProviderAuthority, @DurationMillisLong long timeout)
throws ExecutionException, InterruptedException, TimeoutException {
if (cloudMediaProviderAuthority == null) {
return null;
}
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
final Bundle out = resolver.call(getMediaCollectionInfoUri(cloudMediaProviderAuthority),
METHOD_GET_MEDIA_COLLECTION_INFO, /* arg */ null, /* extras */ null);
return (out == null) ? null : out.getString(ACCOUNT_NAME);
});
return (timeout > 0) ? future.get(timeout, MILLISECONDS) : future.get();
}
}