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