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 android.photopicker.cts;
18 
19 import static android.Manifest.permission.READ_DEVICE_CONFIG;
20 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
21 import static android.photopicker.cts.PhotoPickerBaseTest.INVALID_CLOUD_PROVIDER;
22 import static android.photopicker.cts.PickerProviderMediaGenerator.syncCloudProvider;
23 import static android.photopicker.cts.util.PhotoPickerUiUtils.findAddButton;
24 import static android.photopicker.cts.util.PhotoPickerUiUtils.findItemList;
25 
26 import static com.google.common.truth.Truth.assertThat;
27 import static com.google.common.truth.Truth.assertWithMessage;
28 
29 import android.app.UiAutomation;
30 import android.content.ClipData;
31 import android.content.Context;
32 import android.os.UserHandle;
33 import android.provider.DeviceConfig;
34 import android.provider.MediaStore;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.util.Pair;
38 
39 import androidx.annotation.NonNull;
40 import androidx.annotation.Nullable;
41 import androidx.test.InstrumentationRegistry;
42 import androidx.test.uiautomator.UiDevice;
43 import androidx.test.uiautomator.UiObject;
44 
45 import com.android.modules.utils.build.SdkLevel;
46 
47 import java.io.IOException;
48 import java.util.ArrayList;
49 import java.util.Collections;
50 import java.util.List;
51 
52 public class PhotoPickerCloudUtils {
53     public static final String NAMESPACE_MEDIAPROVIDER = "mediaprovider";
54     public static final String NAMESPACE_STORAGE_NATIVE_BOOT = "storage_native_boot";
55     private static final String KEY_ALLOWED_CLOUD_PROVIDERS = "allowed_cloud_providers";
56     private static final String KEY_CLOUD_MEDIA_FEATURE_ENABLED = "cloud_media_feature_enabled";
57     private static final String KEY_PICKER_PICK_IMAGES_PRELOAD =
58             "picker_pick_images_preload_selected";
59     private static final String DISABLE_DEVICE_CONFIG_SYNC =
60             "cmd device_config set_sync_disabled_for_tests %s";
61     private static final String DISABLE_DEVICE_CONFIG_SYNC_MODE = "until_reboot";
62     private static final String TAG = "PickerCloudUtils";
63 
64     /**
65      * Device config is reset from the server periodically. When tests override device config, it is
66      * not a sticky change. The device config may be reset to server values at any point - even
67      * while a test is running. In order to prevent unwanted device config resets, this method
68      * disables device config syncs until device reboot.
69      */
disableDeviceConfigSync()70     static void disableDeviceConfigSync() {
71         try {
72             UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
73                     .executeShellCommand(String.format(DISABLE_DEVICE_CONFIG_SYNC,
74                             DISABLE_DEVICE_CONFIG_SYNC_MODE));
75         } catch (IOException e) {
76             Log.e(TAG, "Could not disable device_config sync. "
77                     + "Device config may reset to server values at any point during test runs.", e);
78         }
79     }
extractMediaIds(ClipData clipData, int minCount)80     public static List<String> extractMediaIds(ClipData clipData, int minCount) {
81         final int count = clipData.getItemCount();
82         assertThat(count).isAtLeast(minCount);
83 
84         final List<String> mediaIds = new ArrayList<>();
85         for (int i = 0; i < count; i++) {
86             mediaIds.add(clipData.getItemAt(i).getUri().getLastPathSegment());
87         }
88 
89         return mediaIds;
90     }
91 
selectAndAddPickerMedia(UiDevice uiDevice, int maxCount)92     public static void selectAndAddPickerMedia(UiDevice uiDevice, int maxCount)
93             throws Exception {
94         final List<UiObject> itemList = findItemList(maxCount);
95         for (int i = 0; i < itemList.size(); i++) {
96             final UiObject item = itemList.get(i);
97             item.click();
98             uiDevice.waitForIdle();
99         }
100 
101         final UiObject addButton = findAddButton();
102         addButton.click();
103         uiDevice.waitForIdle();
104     }
105 
fetchPickerMedia(GetResultActivity activity, UiDevice uiDevice, int maxCount)106     public static ClipData fetchPickerMedia(GetResultActivity activity, UiDevice uiDevice,
107             int maxCount) throws Exception {
108         selectAndAddPickerMedia(uiDevice, maxCount);
109 
110         return activity.getResult().data.getClipData();
111     }
112 
initCloudProviderWithImage( Context context, PickerProviderMediaGenerator.MediaGenerator mediaGenerator, String authority, Pair<String, String>... mediaPairs)113     public static void initCloudProviderWithImage(
114             Context context, PickerProviderMediaGenerator.MediaGenerator mediaGenerator,
115             String authority, Pair<String, String>... mediaPairs) throws Exception {
116         PhotoPickerBaseTest.setCloudProvider(authority);
117         assertThat(MediaStore.isCurrentCloudMediaProviderAuthority(context.getContentResolver(),
118                 authority)).isTrue();
119 
120         for (Pair<String, String> pair : mediaPairs) {
121             addImage(mediaGenerator, pair.first, pair.second);
122         }
123 
124         syncCloudProvider(context);
125     }
126 
addImage(PickerProviderMediaGenerator.MediaGenerator generator, String localId, String cloudId)127     public static void addImage(PickerProviderMediaGenerator.MediaGenerator generator,
128             String localId, String cloudId)
129             throws Exception {
130         final long imageSizeBytes = 107684;
131         generator.addMedia(localId, cloudId, /* albumId */ null, "image/jpeg",
132                 /* mimeTypeExtension */ 0, imageSizeBytes, /* isFavorite */ false,
133                 R.raw.lg_g4_iso_800_jpg);
134     }
135 
containsExcept(List<String> mediaIds, String contained, String notContained)136     public static void containsExcept(List<String> mediaIds, String contained,
137             String notContained) {
138         assertThat(mediaIds).contains(contained);
139         assertThat(mediaIds).containsNoneIn(Collections.singletonList(notContained));
140     }
141 
142     /**
143      * @return true if cloud is enabled in the device config of the given namespace,
144      * otherwise false.
145      */
isCloudMediaEnabled(@onNull String namespace)146     public static boolean isCloudMediaEnabled(@NonNull String namespace) {
147         return Boolean.parseBoolean(readDeviceConfigProp(namespace,
148                 KEY_CLOUD_MEDIA_FEATURE_ENABLED));
149     }
150 
151     /**
152      * @return the allowed providers in the device config of the given namespace.
153      */
154     @Nullable
getAllowedProvidersDeviceConfig(@onNull String namespace)155     static String getAllowedProvidersDeviceConfig(@NonNull String namespace) {
156         return readDeviceConfigProp(namespace, KEY_ALLOWED_CLOUD_PROVIDERS);
157     }
158 
159     /**
160      * Enables cloud media and sets the allowed cloud provider in the namespace storage_native_boot
161      * and mediaprovider.
162      */
enableCloudMediaAndSetAllowedCloudProviders(@onNull String allowedPackagesJoined)163     static void enableCloudMediaAndSetAllowedCloudProviders(@NonNull String allowedPackagesJoined) {
164         enableCloudMediaAndSetAllowedCloudProviders(NAMESPACE_MEDIAPROVIDER, allowedPackagesJoined);
165         enableCloudMediaAndSetAllowedCloudProviders(
166                 NAMESPACE_STORAGE_NATIVE_BOOT, allowedPackagesJoined);
167     }
168 
169     /**
170      * Enables cloud media and sets the allowed cloud provider in the given namespace.
171      */
enableCloudMediaAndSetAllowedCloudProviders(@onNull String namespace, @NonNull String allowedPackagesJoined)172     static void enableCloudMediaAndSetAllowedCloudProviders(@NonNull String namespace,
173             @NonNull String allowedPackagesJoined) {
174         writeDeviceConfigProp(namespace, KEY_ALLOWED_CLOUD_PROVIDERS, allowedPackagesJoined);
175         assertWithMessage("Failed to update the allowed cloud providers device config")
176                 .that(getAllowedProvidersDeviceConfig(namespace))
177                 .isEqualTo(allowedPackagesJoined);
178 
179         writeDeviceConfigProp(namespace, KEY_CLOUD_MEDIA_FEATURE_ENABLED, true);
180         assertWithMessage("Failed to update the cloud media feature device config")
181                 .that(isCloudMediaEnabled(namespace))
182                 .isTrue();
183     }
184 
185     /**
186      * Disable cloud media in the given namespace.
187      */
disableCloudMediaAndClearAllowedCloudProviders(@onNull String namespace)188     static void disableCloudMediaAndClearAllowedCloudProviders(@NonNull String namespace) {
189         writeDeviceConfigProp(namespace, KEY_CLOUD_MEDIA_FEATURE_ENABLED, false);
190         assertWithMessage("Failed to update the cloud media feature device config")
191                 .that(isCloudMediaEnabled(namespace))
192                 .isFalse();
193 
194         deleteDeviceConfigProp(namespace, KEY_ALLOWED_CLOUD_PROVIDERS);
195         assertWithMessage("Failed to delete the allowed cloud providers device config")
196                 .that(getAllowedProvidersDeviceConfig(namespace))
197                 .isNull();
198     }
199 
enablePickImagesPreload()200     static void enablePickImagesPreload() {
201         writeDeviceConfigProp(NAMESPACE_MEDIAPROVIDER, KEY_PICKER_PICK_IMAGES_PRELOAD, true);
202     }
203 
disablePickImagesPreload()204     static void disablePickImagesPreload() {
205         writeDeviceConfigProp(NAMESPACE_MEDIAPROVIDER, KEY_PICKER_PICK_IMAGES_PRELOAD, false);
206     }
207 
isPickImagesPreloadEnabled()208     static boolean isPickImagesPreloadEnabled() {
209         return Boolean.parseBoolean(
210                 readDeviceConfigProp(NAMESPACE_MEDIAPROVIDER, KEY_PICKER_PICK_IMAGES_PRELOAD)
211         );
212     }
213 
214     @NonNull
getUiAutomation()215     private static UiAutomation getUiAutomation() {
216         return InstrumentationRegistry.getInstrumentation().getUiAutomation();
217     }
218 
219     @Nullable
readDeviceConfigProp(@onNull String namespace, @NonNull String name)220     private static String readDeviceConfigProp(@NonNull String namespace, @NonNull String name) {
221         getUiAutomation().adoptShellPermissionIdentity(READ_DEVICE_CONFIG);
222         try {
223             return DeviceConfig.getProperty(namespace, name);
224         } finally {
225             getUiAutomation().dropShellPermissionIdentity();
226         }
227     }
228 
writeDeviceConfigProp(@onNull String namespace, @NonNull String key, boolean value)229     private static void writeDeviceConfigProp(@NonNull String namespace,
230             @NonNull String key, boolean value) {
231         writeDeviceConfigProp(namespace, key, Boolean.toString(value));
232     }
233 
writeDeviceConfigProp(@onNull String namespace, @NonNull String name, @NonNull String value)234     private static void writeDeviceConfigProp(@NonNull String namespace, @NonNull String name,
235             @NonNull String value) {
236         getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG);
237 
238         try {
239             DeviceConfig.setProperty(namespace, name, value,
240                     /* makeDefault*/ false);
241         } finally {
242             getUiAutomation().dropShellPermissionIdentity();
243         }
244     }
245 
deleteDeviceConfigProp(@onNull String namespace, @NonNull String name)246     private static void deleteDeviceConfigProp(@NonNull String namespace, @NonNull String name) {
247         try {
248             getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG);
249             if (SdkLevel.isAtLeastU()) {
250                 DeviceConfig.deleteProperty(namespace, name);
251             } else {
252                 // DeviceConfig.deleteProperty API is only available from T onwards.
253                 try {
254                     UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
255                             .executeShellCommand(
256                                     String.format("device_config delete %s %s",
257                                             namespace, name));
258                 } catch (IOException e) {
259                     Log.e(TAG, String.format("Could not delete device_config %s / %s",
260                             namespace, name), e);
261                 }
262             }
263         } finally {
264             getUiAutomation().dropShellPermissionIdentity();
265         }
266     }
267 
268     /**
269      * Set cloud provider in the device to the provided authority.
270      * If the provided cloud authority equals {@link PhotoPickerBaseTest#INVALID_CLOUD_PROVIDER},
271      * the cloud provider will not be updated.
272      */
setCloudProvider(@ullable UiDevice sDevice, @Nullable String authority)273     public static void setCloudProvider(@Nullable UiDevice sDevice,
274             @Nullable String authority) throws IOException {
275         if (INVALID_CLOUD_PROVIDER.equals(authority)) {
276             Log.w(TAG, "Cloud provider is invalid. "
277                     + "Ignoring the request to set the cloud provider to invalid");
278             return;
279         }
280         if (authority == null) {
281             sDevice.executeShellCommand(
282                     "content call"
283                             + " --user " + UserHandle.myUserId()
284                             + " --uri content://media/ --method set_cloud_provider --extra"
285                             + " cloud_provider:n:null");
286         } else {
287             sDevice.executeShellCommand(
288                     "content call"
289                             + " --user " + UserHandle.myUserId()
290                             + " --uri content://media/ --method set_cloud_provider --extra"
291                             + " cloud_provider:s:"
292                             + authority);
293         }
294     }
295 
296     /**
297      * @return the current cloud provider.
298      */
getCurrentCloudProvider(UiDevice sDevice)299     public static String getCurrentCloudProvider(UiDevice sDevice) throws IOException {
300         final String out =
301                 sDevice.executeShellCommand(
302                         "content call"
303                                 + " --user " + UserHandle.myUserId()
304                                 + " --uri content://media/ --method get_cloud_provider");
305         return extractCloudProvider(out);
306     }
307 
extractCloudProvider(String out)308     private static String extractCloudProvider(String out) {
309         String[] splitOutput;
310         if (TextUtils.isEmpty(out) || ((splitOutput = out.split("=")).length < 2)) {
311             throw new RuntimeException("Could not get current cloud provider. Output: " + out);
312         }
313         String cloudprovider = splitOutput[1];
314         cloudprovider = cloudprovider.substring(0, cloudprovider.length() - 3);
315         if (cloudprovider.equals("null")) {
316             return null;
317         }
318         return cloudprovider;
319     }
320 }
321