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