1 /*
2  * Copyright (C) 2024 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.Manifest.permission.READ_DEVICE_CONFIG;
20 import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
21 
22 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
23 
24 import static com.android.providers.media.ConfigStore.ConfigStoreImpl.KEY_CLOUD_MEDIA_FEATURE_ENABLED;
25 import static com.android.providers.media.ConfigStore.ConfigStoreImpl.KEY_CLOUD_MEDIA_PROVIDER_ALLOWLIST;
26 
27 import static com.google.common.truth.Truth.assertWithMessage;
28 
29 import android.os.UserHandle;
30 import android.provider.DeviceConfig;
31 import android.text.TextUtils;
32 import android.util.Log;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 import androidx.test.uiautomator.UiDevice;
37 
38 import com.android.modules.utils.build.SdkLevel;
39 
40 import java.io.IOException;
41 
42 public class PhotoPickerCloudTestUtils {
43     private static final String TAG = PhotoPickerCloudTestUtils.class.getSimpleName();
44     static final String INVALID_CLOUD_PROVIDER = "Invalid";
45 
46     /**
47      * @return true if cloud is enabled in the device config of the given namespace,
48      * otherwise false.
49      */
isCloudMediaEnabled(@onNull String namespace)50     static boolean isCloudMediaEnabled(@NonNull String namespace) {
51         return Boolean.parseBoolean(
52                 readDeviceConfigProp(namespace, KEY_CLOUD_MEDIA_FEATURE_ENABLED));
53     }
54 
55     /**
56      * @return the allowed providers in the device config of the given namespace.
57      */
58     @Nullable
getAllowedProvidersDeviceConfig(@onNull String namespace)59     static String getAllowedProvidersDeviceConfig(@NonNull String namespace) {
60         return readDeviceConfigProp(namespace, KEY_CLOUD_MEDIA_PROVIDER_ALLOWLIST);
61     }
62 
63     /**
64      * Enables cloud media and sets the allowed cloud provider in the given namespace.
65      */
enableCloudMediaAndSetAllowedCloudProviders( @onNull String namespace, @NonNull String allowedPackagesJoined)66     public static void enableCloudMediaAndSetAllowedCloudProviders(
67             @NonNull String namespace, @NonNull String allowedPackagesJoined) {
68         writeDeviceConfigProp(namespace, KEY_CLOUD_MEDIA_PROVIDER_ALLOWLIST, allowedPackagesJoined);
69         assertWithMessage("Failed to update the allowed cloud providers device config")
70                 .that(getAllowedProvidersDeviceConfig(namespace))
71                 .isEqualTo(allowedPackagesJoined);
72 
73         writeDeviceConfigProp(namespace, KEY_CLOUD_MEDIA_FEATURE_ENABLED, true);
74         assertWithMessage("Failed to update the cloud media feature device config")
75                 .that(isCloudMediaEnabled(namespace))
76                 .isTrue();
77     }
78 
79     /**
80      * Disable cloud media in the given namespace.
81      */
disableCloudMediaAndClearAllowedCloudProviders(@onNull String namespace)82     static void disableCloudMediaAndClearAllowedCloudProviders(@NonNull String namespace) {
83         writeDeviceConfigProp(namespace, KEY_CLOUD_MEDIA_FEATURE_ENABLED, false);
84         assertWithMessage("Failed to update the cloud media feature device config")
85                 .that(isCloudMediaEnabled(namespace))
86                 .isFalse();
87 
88         deleteDeviceConfigProp(namespace, KEY_CLOUD_MEDIA_PROVIDER_ALLOWLIST);
89         assertWithMessage("Failed to delete the allowed cloud providers device config")
90                 .that(getAllowedProvidersDeviceConfig(namespace))
91                 .isNull();
92     }
93 
94     @Nullable
readDeviceConfigProp(@onNull String namespace, @NonNull String name)95     private static String readDeviceConfigProp(@NonNull String namespace, @NonNull String name) {
96         getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(READ_DEVICE_CONFIG);
97         try {
98             return DeviceConfig.getProperty(namespace, name);
99         } finally {
100             getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
101         }
102     }
103 
writeDeviceConfigProp( @onNull String namespace, @NonNull String key, boolean value)104     private static void writeDeviceConfigProp(
105             @NonNull String namespace, @NonNull String key, boolean value) {
106         writeDeviceConfigProp(namespace, key, Boolean.toString(value));
107     }
108 
writeDeviceConfigProp( @onNull String namespace, @NonNull String name, @NonNull String value)109     private static void writeDeviceConfigProp(
110             @NonNull String namespace, @NonNull String name, @NonNull String value) {
111         getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG);
112 
113         try {
114             DeviceConfig.setProperty(namespace, name, value, /* makeDefault= */ false);
115         } finally {
116             getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
117         }
118     }
119 
deleteDeviceConfigProp(@onNull String namespace, @NonNull String name)120     private static void deleteDeviceConfigProp(@NonNull String namespace, @NonNull String name) {
121         getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG);
122         try {
123             if (SdkLevel.isAtLeastU()) {
124                 DeviceConfig.deleteProperty(namespace, name);
125             } else {
126                 // DeviceConfig.deleteProperty API is only available from U onwards.
127                 try {
128                     UiDevice.getInstance(getInstrumentation())
129                             .executeShellCommand(
130                                     String.format("device_config delete %s %s", namespace, name));
131                 } catch (IOException e) {
132                     Log.e(TAG, String.format("Could not delete device_config %s / %s",
133                             namespace, name), e);
134                 }
135             }
136         } finally {
137             getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
138         }
139     }
140 
141     /**
142      * Set cloud provider in the device to the provided authority.
143      * If the provided cloud authority equals {@link #INVALID_CLOUD_PROVIDER},
144      * the cloud provider will not be updated.
145      */
setCloudProvider(@onNull UiDevice sDevice, @Nullable String authority)146     public static void setCloudProvider(@NonNull UiDevice sDevice,
147             @Nullable String authority) throws IOException {
148         if (INVALID_CLOUD_PROVIDER.equals(authority)) {
149             Log.w(TAG, "Cloud provider is invalid. "
150                     + "Ignoring the request to set the cloud provider to invalid");
151             return;
152         }
153         if (authority == null) {
154             sDevice.executeShellCommand(
155                     "content call"
156                             + " --user " + UserHandle.myUserId()
157                             + " --uri content://media/"
158                             + " --method set_cloud_provider"
159                             + " --extra cloud_provider:n:null");
160         } else {
161             sDevice.executeShellCommand(
162                     "content call"
163                             + " --user " + UserHandle.myUserId()
164                             + " --uri content://media/"
165                             + " --method set_cloud_provider"
166                             + " --extra cloud_provider:s:" + authority);
167         }
168     }
169 
170     /**
171      * @return the current cloud provider.
172      */
getCurrentCloudProvider(UiDevice sDevice)173     public static String getCurrentCloudProvider(UiDevice sDevice) throws IOException {
174         final String shellOutput =
175                 sDevice.executeShellCommand(
176                         "content call"
177                                 + " --user " + UserHandle.myUserId()
178                                 + " --uri content://media/"
179                                 + " --method get_cloud_provider");
180         return extractCloudProvider(shellOutput);
181     }
182 
extractCloudProvider(String shellOutput)183     private static String extractCloudProvider(String shellOutput) {
184         final String[] splitOutput;
185         if (TextUtils.isEmpty(shellOutput) || ((splitOutput = shellOutput.split("=")).length < 2)) {
186             throw new RuntimeException("Could not get current cloud provider. Output: "
187                     + shellOutput);
188         }
189         String cloudProvider = splitOutput[1];
190         cloudProvider = cloudProvider.substring(0, cloudProvider.length() - 3);
191         if (cloudProvider.equals("null")) {
192             return null;
193         }
194         return cloudProvider;
195     }
196 }
197