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