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 androidx.annotation.NonNull;
19 
20 import com.google.common.collect.MapDifference;
21 import com.google.common.collect.Maps;
22 
23 import java.io.BufferedReader;
24 import java.io.StringReader;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Objects;
30 import java.util.Set;
31 
32 /**
33  * A class for interacting with the {@code device_config} service via the shell "cmd" command-line
34  * interface. Some behavior it supports is not available via the Android @SystemApi.
35  * See {@link com.android.providers.settings.DeviceConfigService} for the shell command
36  * implementation details.
37  */
38 public class DeviceConfigShellHelper {
39 
40     /**
41      * Value used with {@link #setSyncModeForTest}, {@link #getSyncDisabled()},
42      * {@link #setSyncDisabled(String)}.
43      */
44     public static final String SYNC_DISABLED_MODE_NONE = "none";
45 
46     /**
47      * Value used with {@link #setSyncModeForTest}, {@link #getSyncDisabled()},
48      * {@link #setSyncDisabled(String)}.
49      */
50     public static final String SYNC_DISABLED_MODE_UNTIL_REBOOT = "until_reboot";
51 
52     /**
53      * Value used with {@link #setSyncModeForTest}, {@link #getSyncDisabled()},
54      * {@link #setSyncDisabled(String)}.
55      */
56     public static final String SYNC_DISABLED_MODE_PERSISTENT = "persistent";
57 
58     private static final String SERVICE_NAME = "device_config";
59 
60     private static final String SHELL_CMD_PREFIX = "cmd " + SERVICE_NAME + " ";
61 
62     @NonNull
63     private final DeviceShellCommandExecutor mShellCommandExecutor;
64 
DeviceConfigShellHelper(DeviceShellCommandExecutor shellCommandExecutor)65     public DeviceConfigShellHelper(DeviceShellCommandExecutor shellCommandExecutor) {
66         mShellCommandExecutor = Objects.requireNonNull(shellCommandExecutor);
67     }
68 
69     /**
70      * Executes "get_sync_disabled_for_tests". Returns the output, expected to be one of
71      * {@link #SYNC_DISABLED_MODE_PERSISTENT}, {@link #SYNC_DISABLED_MODE_UNTIL_REBOOT} or
72      * {@link #SYNC_DISABLED_MODE_NONE}.
73      */
getSyncDisabled()74     public String getSyncDisabled() throws Exception {
75         String cmd = SHELL_CMD_PREFIX + "get_sync_disabled_for_tests";
76         return mShellCommandExecutor.executeToTrimmedString(cmd);
77     }
78 
79     /**
80      * Executes "set_sync_disabled_for_tests". Accepts one of
81      * {@link #SYNC_DISABLED_MODE_PERSISTENT}, {@link #SYNC_DISABLED_MODE_UNTIL_REBOOT} or
82      * {@link #SYNC_DISABLED_MODE_NONE}.
83      */
setSyncDisabled(String syncDisabledMode)84     public void setSyncDisabled(String syncDisabledMode) throws Exception {
85         String cmd = String.format(
86                 SHELL_CMD_PREFIX + "set_sync_disabled_for_tests %s", syncDisabledMode);
87         mShellCommandExecutor.executeToTrimmedString(cmd);
88     }
89 
90     /**
91      * Executes "list" with a namespace.
92      */
list(String namespace)93     public NamespaceEntries list(String namespace) throws Exception {
94         Objects.requireNonNull(namespace);
95 
96         String cmd = String.format(SHELL_CMD_PREFIX + "list %s", namespace);
97         String output = mShellCommandExecutor.executeToTrimmedString(cmd);
98         Map<String, String> keyValues = new HashMap();
99         try (BufferedReader reader = new BufferedReader(new StringReader(output))) {
100             String line;
101             while ((line = reader.readLine()) != null) {
102                 int separatorPos = line.indexOf('=');
103                 String key = line.substring(0, separatorPos);
104                 String value = line.substring(separatorPos + 1);
105                 keyValues.put(key, value);
106             }
107         }
108         return new NamespaceEntries(namespace, keyValues);
109     }
110 
111     /** Executes "put" without the trailing "default" argument. */
put(String namespace, String key, String value)112     public void put(String namespace, String key, String value) throws Exception {
113         put(namespace, key, value, /*makeDefault=*/false);
114     }
115 
116     /** Executes "put". */
put(String namespace, String key, String value, boolean makeDefault)117     public void put(String namespace, String key, String value, boolean makeDefault)
118             throws Exception {
119         String cmd = String.format(SHELL_CMD_PREFIX + "put %s %s %s", namespace, key, value);
120         if (makeDefault) {
121             cmd += " default";
122         }
123         mShellCommandExecutor.executeToTrimmedString(cmd);
124     }
125 
126     /** Executes "delete". */
delete(String namespace, String key)127     public void delete(String namespace, String key) throws Exception {
128         String cmd = String.format(SHELL_CMD_PREFIX + "delete %s %s", namespace, key);
129         mShellCommandExecutor.executeToTrimmedString(cmd);
130     }
131 
132     /**
133      * A test helper method that captures the current sync mode and set of namespace values and sets
134      * the current sync mode. See {@link #restoreDeviceConfigStateForTest(PreTestState)}.
135      */
setSyncModeForTest(String syncMode, String... namespacesToSave)136     public PreTestState setSyncModeForTest(String syncMode, String... namespacesToSave)
137             throws Exception {
138         List<NamespaceEntries> savedValues = new ArrayList<>();
139         for (String namespacetoSave : namespacesToSave) {
140             NamespaceEntries namespaceValues = list(namespacetoSave);
141             savedValues.add(namespaceValues);
142         }
143         PreTestState preTestState = new PreTestState(getSyncDisabled(), savedValues);
144         setSyncDisabled(syncMode);
145         return preTestState;
146     }
147 
148     /**
149      * Restores the sync mode after a test. See {@link #setSyncModeForTest}.
150      */
restoreDeviceConfigStateForTest(PreTestState restoreState)151     public void restoreDeviceConfigStateForTest(PreTestState restoreState) throws Exception {
152         for (NamespaceEntries oldEntries : restoreState.mSavedValues) {
153             NamespaceEntries currentEntries = list(oldEntries.namespace);
154 
155             MapDifference<String, String> difference =
156                     Maps.difference(oldEntries.keyValues, currentEntries.keyValues);
157             deleteAll(oldEntries.namespace, difference.entriesOnlyOnRight());
158             putAll(oldEntries.namespace, difference.entriesOnlyOnLeft());
159             Map<String, String> entriesToUpdate =
160                     subMap(oldEntries.keyValues, difference.entriesDiffering().keySet());
161             putAll(oldEntries.namespace, entriesToUpdate);
162         }
163         setSyncDisabled(restoreState.mSyncDisabledMode);
164     }
165 
subMap(Map<X, Y> keyValues, Set<X> keySet)166     private static <X, Y> Map<X, Y> subMap(Map<X, Y> keyValues, Set<X> keySet) {
167         return Maps.filterKeys(keyValues, keySet::contains);
168     }
169 
putAll(String namespace, Map<String, String> entriesToAdd)170     private void putAll(String namespace, Map<String, String> entriesToAdd) throws Exception {
171         for (Map.Entry<String, String> entryToAdd : entriesToAdd.entrySet()) {
172             put(namespace, entryToAdd.getKey(), entryToAdd.getValue());
173         }
174     }
175 
deleteAll(String namespace, Map<String, String> entriesToDelete)176     private void deleteAll(String namespace, Map<String, String> entriesToDelete) throws Exception {
177         for (Map.Entry<String, String> entryToDelete : entriesToDelete.entrySet()) {
178             delete(namespace, entryToDelete.getKey());
179         }
180     }
181 
182     /** Opaque saved state information. */
183     public static class PreTestState {
184         private final String mSyncDisabledMode;
185         private final List<NamespaceEntries> mSavedValues = new ArrayList<>();
186 
PreTestState(String syncDisabledMode, List<NamespaceEntries> values)187         private PreTestState(String syncDisabledMode, List<NamespaceEntries> values) {
188             mSyncDisabledMode = syncDisabledMode;
189             mSavedValues.addAll(values);
190         }
191     }
192 
193     public static class NamespaceEntries {
194         public final String namespace;
195         public final Map<String, String> keyValues = new HashMap<>();
196 
NamespaceEntries(String namespace, Map<String, String> keyValues)197         public NamespaceEntries(String namespace, Map<String, String> keyValues) {
198             this.namespace = Objects.requireNonNull(namespace);
199             this.keyValues.putAll(Objects.requireNonNull(keyValues));
200         }
201     }
202 }
203