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