1 /*
2  * Copyright (C) 2021 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 package android.app.time.cts.shell;
17 
18 import static org.junit.Assert.assertEquals;
19 import static org.junit.Assert.assertFalse;
20 import static org.junit.Assert.assertTrue;
21 import static org.junit.Assert.fail;
22 
23 import java.util.Collections;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Objects;
28 import java.util.regex.Matcher;
29 import java.util.regex.Pattern;
30 
31 /**
32  * A class for interacting with the fake {@link android.service.timezone.TimeZoneProviderService} in
33  * the fake TimeZoneProviderService app.
34  */
35 public final class FakeTimeZoneProviderAppShellHelper {
36 
37     /** The name of the app's APK. */
38     public static final String FAKE_TZPS_APP_APK = "CtsFakeTimeZoneProvidersApp.apk";
39 
40     /** The package name of the app. */
41     public static final String FAKE_TZPS_APP_PACKAGE = "com.android.time.cts.fake_tzps_app";
42 
43     /** The ID of the primary location time zone provider. */
44     public static final String FAKE_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_ID =
45             "FakeLocationTimeZoneProviderService1";
46 
47     /** The ID of the secondary location time zone provider. */
48     public static final String FAKE_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_ID =
49                     "FakeLocationTimeZoneProviderService2";
50 
51     // The following constant values correspond to enum values from
52     // frameworks/base/core/proto/android/app/location_time_zone_manager.proto
53     public static final int PROVIDER_STATE_UNKNOWN = 0;
54     public static final int PROVIDER_STATE_INITIALIZING = 1;
55     public static final int PROVIDER_STATE_CERTAIN = 2;
56     public static final int PROVIDER_STATE_UNCERTAIN = 3;
57     public static final int PROVIDER_STATE_DISABLED = 4;
58     public static final int PROVIDER_STATE_PERM_FAILED = 5;
59     public static final int PROVIDER_STATE_DESTROYED = 6;
60 
61     private static final String METHOD_GET_STATE = "get_state";
62     private static final String CALL_RESULT_KEY_GET_STATE_STATE = "state";
63     private static final String METHOD_REPORT_PERMANENT_FAILURE = "perm_fail";
64     private static final String METHOD_REPORT_UNCERTAIN = "uncertain";
65     private static final String METHOD_REPORT_UNCERTAIN_LEGACY = "uncertain_legacy";
66     private static final String METHOD_REPORT_SUCCESS = "success";
67     private static final String METHOD_REPORT_SUCCESS_LEGACY = "success_legacy";
68     private static final String METHOD_PING = "ping";
69 
70     /** A single string, comma separated, may be empty. */
71     private static final String CALL_EXTRA_KEY_SUCCESS_SUGGESTION_ZONE_IDS = "zone_ids";
72 
73     /** A provider's location detection status. */
74     private static final String CALL_EXTRA_KEY_LOCATION_DETECTION_STATUS =
75             "location_detection_status";
76     /** A provider's connectivity status. */
77     private static final String CALL_EXTRA_KEY_CONNECTIVITY_STATUS = "connectivity_status";
78     /** A provider's time zone resolution status. */
79     private static final String CALL_EXTRA_KEY_TIME_ZONE_RESOLUTION_STATUS =
80             "time_zone_resolution_status";
81 
82     public static final String DEPENDENCY_STATUS_OK = "OK";
83     public static final String DEPENDENCY_STATUS_NOT_APPLICABLE = "NOT_APPLICABLE";
84     public static final String DEPENDENCY_STATUS_TEMPORARILY_UNAVAILABLE =
85             "TEMPORARILY_UNAVAILABLE";
86     public static final String DEPENDENCY_STATUS_BLOCKED_BY_ENVIRONMENT = "BLOCKED_BY_ENVIRONMENT";
87     public static final String DEPENDENCY_STATUS_BLOCKED_BY_SETTINGS = "BLOCKED_BY_SETTINGS";
88 
89     public static final String OPERATION_STATUS_NOT_APPLICABLE = "NOT_APPLICABLE";
90     public static final String OPERATION_STATUS_OK = "OK";
91     public static final String OPERATION_STATUS_FAILED = "FAILED";
92 
93     private static final String SHELL_COMMAND_PREFIX = "content ";
94     private static final String AUTHORITY = "faketzpsapp ";
95 
96     private final DeviceShellCommandExecutor mShellCommandExecutor;
97 
FakeTimeZoneProviderAppShellHelper(DeviceShellCommandExecutor shellCommandExecutor)98     public FakeTimeZoneProviderAppShellHelper(DeviceShellCommandExecutor shellCommandExecutor) {
99         mShellCommandExecutor = Objects.requireNonNull(shellCommandExecutor);
100     }
101 
102     /**
103      * Throws an exception if the app is not installed / available within a reasonable time.
104      */
waitForInstallation()105     public void waitForInstallation() throws Exception {
106         long timeoutMs = 10000;
107         long delayUntilMillis = System.currentTimeMillis() + timeoutMs;
108         while (System.currentTimeMillis() <= delayUntilMillis) {
109             try {
110                 ping();
111                 return;
112             } catch (AssertionError e) {
113                 // Not present yet.
114             }
115             Thread.sleep(100);
116         }
117         fail("Installation did not happen in time");
118     }
119 
getPrimaryLocationProviderHelper()120     public FakeTimeZoneProviderShellHelper getPrimaryLocationProviderHelper() {
121         return getProviderHelper(FAKE_PRIMARY_LOCATION_TIME_ZONE_PROVIDER_ID);
122     }
123 
getSecondaryLocationProviderHelper()124     public FakeTimeZoneProviderShellHelper getSecondaryLocationProviderHelper() {
125         return getProviderHelper(FAKE_SECONDARY_LOCATION_TIME_ZONE_PROVIDER_ID);
126     }
127 
getProviderHelper(String providerId)128     private FakeTimeZoneProviderShellHelper getProviderHelper(String providerId) {
129         return new FakeTimeZoneProviderShellHelper(providerId);
130     }
131 
132     /**
133      * A helper for interacting with a specific {@link
134      * android.service.timezone.TimeZoneProviderService}.
135      */
136     public final class FakeTimeZoneProviderShellHelper {
137 
138         private final String mProviderId;
139 
FakeTimeZoneProviderShellHelper(String providerId)140         private FakeTimeZoneProviderShellHelper(String providerId) {
141             mProviderId = Objects.requireNonNull(providerId);
142         }
143 
144         /** Causes {@link TimeZoneProviderService#reportUncertain()} to be called. */
reportUncertainLegacy()145         public void reportUncertainLegacy() throws Exception {
146             Map<String, String> extras = new HashMap<>();
147             executeContentProviderCall(mProviderId, METHOD_REPORT_UNCERTAIN_LEGACY, extras);
148         }
149 
150         /**
151          * Causes {@link TimeZoneProviderService#reportUncertain(TimeZoneProviderStatus)} to be
152          * called.
153          */
reportUncertain(String locationDetectionStatus, String connectivityStatus, String timeZoneResolutionStatus)154         public void reportUncertain(String locationDetectionStatus,
155                 String connectivityStatus, String timeZoneResolutionStatus) throws Exception {
156             Map<String, String> extras = new HashMap<>();
157             extras.put(CALL_EXTRA_KEY_LOCATION_DETECTION_STATUS,
158                     Objects.requireNonNull(locationDetectionStatus));
159             extras.put(CALL_EXTRA_KEY_CONNECTIVITY_STATUS,
160                     Objects.requireNonNull(connectivityStatus));
161             extras.put(CALL_EXTRA_KEY_TIME_ZONE_RESOLUTION_STATUS,
162                     Objects.requireNonNull(timeZoneResolutionStatus));
163 
164             executeContentProviderCall(mProviderId, METHOD_REPORT_UNCERTAIN, extras);
165         }
166 
reportPermanentFailure()167         public void reportPermanentFailure() throws Exception {
168             executeContentProviderCall(mProviderId, METHOD_REPORT_PERMANENT_FAILURE, null);
169         }
170 
171         /**
172          * Causes TimeZoneProviderService.reportSuggestion(TimeZoneProviderSuggestion) to be called.
173          */
reportSuccessLegacy(String zoneId)174         public void reportSuccessLegacy(String zoneId) throws Exception {
175             reportSuccessLegacy(Collections.singletonList(zoneId));
176         }
177 
178         /**
179          * Causes TimeZoneProviderService.reportSuggestion(TimeZoneProviderSuggestion) to be called.
180          */
reportSuccessLegacy(List<String> zoneIds)181         public void reportSuccessLegacy(List<String> zoneIds) throws Exception {
182             String zoneIdsExtra = String.join(",", zoneIds);
183             Map<String, String> extras = new HashMap<>();
184             extras.put(CALL_EXTRA_KEY_SUCCESS_SUGGESTION_ZONE_IDS, zoneIdsExtra);
185 
186             executeContentProviderCall(mProviderId, METHOD_REPORT_SUCCESS_LEGACY, extras);
187         }
188 
189         /**
190          * Causes TimeZoneProviderService.reportSuggestion(TimeZoneProviderSuggestion,
191          * TimeZoneProviderStatus) to be called.
192          */
reportSuccess(String zoneId, String locationDetectionStatus, String connectivityStatus)193         public void reportSuccess(String zoneId, String locationDetectionStatus,
194                 String connectivityStatus) throws Exception {
195             reportSuccess(Collections.singletonList(zoneId), locationDetectionStatus,
196                     connectivityStatus);
197         }
198 
199         /**
200          * Causes TimeZoneProviderService#reportSuggestion(TimeZoneProviderSuggestion,
201          * TimeZoneProviderStatus) to be called.
202          */
reportSuccess(List<String> zoneIds, String locationDetectionStatus, String connectivityStatus)203         public void reportSuccess(List<String> zoneIds, String locationDetectionStatus,
204                 String connectivityStatus) throws Exception {
205             String zoneIdsExtra = String.join(",", zoneIds);
206             Map<String, String> extras = new HashMap<>();
207             extras.put(CALL_EXTRA_KEY_SUCCESS_SUGGESTION_ZONE_IDS, zoneIdsExtra);
208             extras.put(CALL_EXTRA_KEY_LOCATION_DETECTION_STATUS,
209                     Objects.requireNonNull(locationDetectionStatus));
210             extras.put(CALL_EXTRA_KEY_CONNECTIVITY_STATUS,
211                     Objects.requireNonNull(connectivityStatus));
212             extras.put(CALL_EXTRA_KEY_TIME_ZONE_RESOLUTION_STATUS, OPERATION_STATUS_OK);
213 
214             executeContentProviderCall(mProviderId, METHOD_REPORT_SUCCESS, extras);
215         }
216 
getState()217         public int getState() throws Exception {
218             String stateResult = executeContentProviderCall(mProviderId, METHOD_GET_STATE, null);
219             Pattern pattern = Pattern.compile(".*" + CALL_RESULT_KEY_GET_STATE_STATE + "=(.).*");
220             Matcher matcher = pattern.matcher(stateResult);
221             if (!matcher.matches()) {
222                 throw new RuntimeException("Unknown result format: " + stateResult);
223             }
224             return Integer.parseInt(matcher.group(1));
225         }
226 
assertCurrentState(int expectedState)227         public void assertCurrentState(int expectedState) throws Exception {
228             assertEquals(expectedState, getState());
229         }
230 
exists()231         public boolean exists() throws Exception {
232             try {
233                 getState();
234                 return true;
235             } catch (AssertionError e) {
236                 return false;
237             }
238         }
239 
assertCreated()240         public void assertCreated() throws Exception {
241             assertTrue(exists());
242         }
243 
assertNotCreated()244         public void assertNotCreated() throws Exception {
245             assertFalse(exists());
246         }
247     }
248 
ping()249     private void ping() throws Exception {
250         String cmd = String.format("call --uri content://%s --method %s", AUTHORITY, METHOD_PING);
251         mShellCommandExecutor.executeToTrimmedString(SHELL_COMMAND_PREFIX + cmd);
252     }
253 
executeContentProviderCall( String providerId, String method, Map<String, String> extras)254     private String executeContentProviderCall(
255             String providerId, String method, Map<String, String> extras) throws Exception {
256         String cmd = String.format("call --uri content://%s --method %s --arg %s",
257                 AUTHORITY, method, providerId);
258         if (extras != null) {
259             for (Map.Entry<String, String> entry : extras.entrySet()) {
260                 cmd += String.format(" --extra %s:s:%s", entry.getKey(), entry.getValue());
261             }
262         }
263         return mShellCommandExecutor.executeToTrimmedString(SHELL_COMMAND_PREFIX + cmd);
264     }
265 }
266