diff options
5 files changed, 604 insertions, 0 deletions
diff --git a/cmds/device_config/Android.mk b/cmds/device_config/Android.mk new file mode 100644 index 000000000000..4041e01927df --- /dev/null +++ b/cmds/device_config/Android.mk @@ -0,0 +1,10 @@ +# Copyright 2018 The Android Open Source Project +# +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) +LOCAL_MODULE := device_config +LOCAL_SRC_FILES := device_config +LOCAL_MODULE_CLASS := EXECUTABLES +LOCAL_MODULE_TAGS := optional +include $(BUILD_PREBUILT) diff --git a/cmds/device_config/device_config b/cmds/device_config/device_config new file mode 100755 index 000000000000..a949bd528263 --- /dev/null +++ b/cmds/device_config/device_config @@ -0,0 +1,2 @@ +#!/system/bin/sh +cmd device_config "$@" diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java new file mode 100644 index 000000000000..352091804de2 --- /dev/null +++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java @@ -0,0 +1,355 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.providers.settings; + +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.ActivityManager; +import android.content.IContentProvider; +import android.net.Uri; +import android.os.Binder; +import android.os.Bundle; +import android.os.Process; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ShellCallback; +import android.os.ShellCommand; +import android.provider.Settings; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Receives shell commands from the command line related to device config flags, and dispatches them + * to the SettingsProvider. + * + * @hide + */ +@SystemApi +public final class DeviceConfigService extends Binder { + /** + * TODO(b/113100523): Move this to DeviceConfig.java when it is added, and expose it as a System + * API. + */ + private static final Uri CONFIG_CONTENT_URI = + Uri.parse("content://" + Settings.AUTHORITY + "/config"); + + final SettingsProvider mProvider; + + public DeviceConfigService(SettingsProvider provider) { + mProvider = provider; + } + + @Override + public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ShellCallback callback, ResultReceiver resultReceiver) { + (new MyShellCommand(mProvider)).exec(this, in, out, err, args, callback, resultReceiver); + } + + static final class MyShellCommand extends ShellCommand { + final SettingsProvider mProvider; + + enum CommandVerb { + UNSPECIFIED, + GET, + PUT, + DELETE, + LIST, + RESET, + } + + MyShellCommand(SettingsProvider provider) { + mProvider = provider; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null || "help".equals(cmd) || "-h".equals(cmd)) { + onHelp(); + return -1; + } + + final PrintWriter perr = getErrPrintWriter(); + boolean isValid = false; + CommandVerb verb; + if ("get".equalsIgnoreCase(cmd)) { + verb = CommandVerb.GET; + } else if ("put".equalsIgnoreCase(cmd)) { + verb = CommandVerb.PUT; + } else if ("delete".equalsIgnoreCase(cmd)) { + verb = CommandVerb.DELETE; + } else if ("list".equalsIgnoreCase(cmd)) { + verb = CommandVerb.LIST; + if (peekNextArg() == null) { + isValid = true; + } + } else if ("reset".equalsIgnoreCase(cmd)) { + verb = CommandVerb.RESET; + } else { + // invalid + perr.println("Invalid command: " + cmd); + return -1; + } + + int resetMode = -1; + boolean makeDefault = false; + String namespace = null; + String key = null; + String value = null; + String arg = null; + while ((arg = getNextArg()) != null) { + if (verb == CommandVerb.RESET) { + if (resetMode == -1) { + if ("untrusted_defaults".equalsIgnoreCase(arg)) { + resetMode = Settings.RESET_MODE_UNTRUSTED_DEFAULTS; + } else if ("untrusted_clear".equalsIgnoreCase(arg)) { + resetMode = Settings.RESET_MODE_UNTRUSTED_CHANGES; + } else if ("trusted_defaults".equalsIgnoreCase(arg)) { + resetMode = Settings.RESET_MODE_TRUSTED_DEFAULTS; + } else { + // invalid + perr.println("Invalid reset mode: " + arg); + return -1; + } + if (peekNextArg() == null) { + isValid = true; + } + } else { + namespace = arg; + if (peekNextArg() == null) { + isValid = true; + } else { + // invalid + perr.println("Too many arguments"); + return -1; + } + } + } else if (namespace == null) { + namespace = arg; + if (verb == CommandVerb.LIST) { + if (peekNextArg() == null) { + isValid = true; + } else { + // invalid + perr.println("Too many arguments"); + return -1; + } + } + } else if (key == null) { + key = arg; + if ((verb == CommandVerb.GET || verb == CommandVerb.DELETE)) { + if (peekNextArg() == null) { + isValid = true; + } else { + // invalid + perr.println("Too many arguments"); + return -1; + } + } + } else if (value == null) { + value = arg; + if (verb == CommandVerb.PUT && peekNextArg() == null) { + isValid = true; + } + } else if ("default".equalsIgnoreCase(arg)) { + makeDefault = true; + if (verb == CommandVerb.PUT && peekNextArg() == null) { + isValid = true; + } else { + // invalid + perr.println("Too many arguments"); + return -1; + } + } + } + + if (!isValid) { + perr.println("Bad arguments"); + return -1; + } + + final IContentProvider iprovider = mProvider.getIContentProvider(); + final PrintWriter pout = getOutPrintWriter(); + switch (verb) { + case GET: + pout.println(get(iprovider, namespace, key)); + break; + case PUT: + put(iprovider, namespace, key, value, makeDefault); + break; + case DELETE: + pout.println(delete(iprovider, namespace, key) + ? "Successfully deleted " + key + " from " + namespace + : "Failed to delete " + key + " from " + namespace); + break; + case LIST: + for (String line : list(iprovider, namespace)) { + pout.println(line); + } + break; + case RESET: + reset(iprovider, resetMode, namespace); + break; + default: + perr.println("Unspecified command"); + return -1; + } + return 0; + } + + @Override + public void onHelp() { + PrintWriter pw = getOutPrintWriter(); + pw.println("Device Config (device_config) commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(" get NAMESPACE KEY"); + pw.println(" Retrieve the current value of KEY from the given NAMESPACE."); + pw.println(" put NAMESPACE KEY VALUE [default]"); + pw.println(" Change the contents of KEY to VALUE for the given NAMESPACE."); + pw.println(" {default} to set as the default value."); + pw.println(" delete NAMESPACE KEY"); + pw.println(" Delete the entry for KEY for the given NAMESPACE."); + pw.println(" list [NAMESPACE]"); + pw.println(" Print all keys and values defined, optionally for the given " + + "NAMESPACE."); + pw.println(" reset RESET_MODE [NAMESPACE]"); + pw.println(" Reset all flag values, optionally for a NAMESPACE, according to " + + "RESET_MODE."); + pw.println(" RESET_MODE is one of {untrusted_defaults, untrusted_clear, " + + "trusted_defaults}"); + pw.println(" NAMESPACE limits which flags are reset if provided, otherwise all " + + "flags are reset"); + } + + private String get(IContentProvider provider, String namespace, String key) { + String compositeKey = namespace + "/" + key; + String result = null; + try { + Bundle args = new Bundle(); + args.putInt(Settings.CALL_METHOD_USER_KEY, + ActivityManager.getService().getCurrentUser().id); + Bundle b = provider.call(resolveCallingPackage(), Settings.CALL_METHOD_GET_CONFIG, + compositeKey, args); + if (b != null) { + result = b.getPairValue(); + } + } catch (RemoteException e) { + throw new RuntimeException("Failed in IPC", e); + } + return result; + } + + private void put(IContentProvider provider, String namespace, String key, String value, + boolean makeDefault) { + String compositeKey = namespace + "/" + key; + + try { + Bundle args = new Bundle(); + args.putString(Settings.NameValueTable.VALUE, value); + args.putInt(Settings.CALL_METHOD_USER_KEY, + ActivityManager.getService().getCurrentUser().id); + if (makeDefault) { + args.putBoolean(Settings.CALL_METHOD_MAKE_DEFAULT_KEY, true); + } + provider.call(resolveCallingPackage(), Settings.CALL_METHOD_PUT_CONFIG, + compositeKey, args); + } catch (RemoteException e) { + throw new RuntimeException("Failed in IPC", e); + } + } + + private boolean delete(IContentProvider provider, String namespace, String key) { + String compositeKey = namespace + "/" + key; + boolean success; + + try { + Bundle args = new Bundle(); + args.putInt(Settings.CALL_METHOD_USER_KEY, + ActivityManager.getService().getCurrentUser().id); + Bundle b = provider.call(resolveCallingPackage(), + Settings.CALL_METHOD_DELETE_CONFIG, compositeKey, args); + success = (b != null && b.getInt(SettingsProvider.RESULT_ROWS_DELETED) == 1); + } catch (RemoteException e) { + throw new RuntimeException("Failed in IPC", e); + } + return success; + } + + private List<String> list(IContentProvider provider, @Nullable String namespace) { + final ArrayList<String> lines = new ArrayList<>(); + + try { + Bundle args = new Bundle(); + args.putInt(Settings.CALL_METHOD_USER_KEY, + ActivityManager.getService().getCurrentUser().id); + if (namespace != null) { + args.putString(Settings.CALL_METHOD_PREFIX_KEY, namespace); + } + Bundle b = provider.call(resolveCallingPackage(), + Settings.CALL_METHOD_LIST_CONFIG, null, args); + if (b != null) { + Map<String, String> flagsToValues = + (HashMap) b.getSerializable(Settings.NameValueTable.VALUE); + for (String key : flagsToValues.keySet()) { + lines.add(key + "=" + flagsToValues.get(key)); + } + } + + Collections.sort(lines); + } catch (RemoteException e) { + throw new RuntimeException("Failed in IPC", e); + } + return lines; + } + + private void reset(IContentProvider provider, int resetMode, @Nullable String namespace) { + try { + Bundle args = new Bundle(); + args.putInt(Settings.CALL_METHOD_USER_KEY, + ActivityManager.getService().getCurrentUser().id); + args.putInt(Settings.CALL_METHOD_RESET_MODE_KEY, resetMode); + args.putString(Settings.CALL_METHOD_PREFIX_KEY, namespace); + provider.call( + resolveCallingPackage(), Settings.CALL_METHOD_RESET_CONFIG, null, args); + } catch (RemoteException e) { + throw new RuntimeException("Failed in IPC", e); + } + } + + private static String resolveCallingPackage() { + switch (Binder.getCallingUid()) { + case Process.ROOT_UID: { + return "root"; + } + + case Process.SHELL_UID: { + return "com.android.shell"; + } + + default: { + return null; + } + } + } + } +} diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 140a5a3f8590..424368d2600c 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -335,6 +335,7 @@ public class SettingsProvider extends ContentProvider { startWatchingUserRestrictionChanges(); }); ServiceManager.addService("settings", new SettingsService(this)); + ServiceManager.addService("device_config", new DeviceConfigService(this)); return true; } diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java new file mode 100644 index 000000000000..59de6a7e64b9 --- /dev/null +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/DeviceConfigServiceTest.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.providers.settings; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; + +import static org.junit.Assert.assertNotNull; + +import android.content.ContentResolver; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import libcore.io.Streams; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Tests for {@link DeviceConfigService}. + */ +@RunWith(AndroidJUnit4.class) +public class DeviceConfigServiceTest { + /** + * TODO(b/113100523): Move this to DeviceConfig.java when it is added, and expose it as a System + * API. + */ + private static final Uri CONFIG_CONTENT_URI = + Uri.parse("content://" + Settings.AUTHORITY + "/config"); + private static final String sNamespace = "namespace1"; + private static final String sKey = "key1"; + private static final String sValue = "value1"; + + private ContentResolver mContentResolver; + + @Before + public void setUp() { + mContentResolver = InstrumentationRegistry.getContext().getContentResolver(); + } + + @After + public void cleanUp() { + deleteFromContentProvider(mContentResolver, sNamespace, sKey); + } + + @Test + public void testPut() throws Exception { + final String newNamespace = "namespace2"; + final String newValue = "value2"; + + String result = getFromContentProvider(mContentResolver, sNamespace, sKey); + assertNull(result); + + try { + executeShellCommand("device_config put " + sNamespace + " " + sKey + " " + sValue); + executeShellCommand("device_config put " + newNamespace + " " + sKey + " " + newValue); + + result = getFromContentProvider(mContentResolver, sNamespace, sKey); + assertEquals(sValue, result); + result = getFromContentProvider(mContentResolver, newNamespace, sKey); + assertEquals(newValue, result); + } finally { + deleteFromContentProvider(mContentResolver, newNamespace, sKey); + } + } + + @Test + public void testPut_invalidArgs() throws Exception { + // missing sNamespace + executeShellCommand("device_config put " + sKey + " " + sValue); + String result = getFromContentProvider(mContentResolver, sNamespace, sKey); + // still null + assertNull(result); + + // too many arguments + executeShellCommand( + "device_config put " + sNamespace + " " + sKey + " " + sValue + " extra_arg"); + result = getFromContentProvider(mContentResolver, sNamespace, sKey); + // still null + assertNull(result); + } + + @Test + public void testDelete() throws Exception { + final String newNamespace = "namespace2"; + + putWithContentProvider(mContentResolver, sNamespace, sKey, sValue); + putWithContentProvider(mContentResolver, newNamespace, sKey, sValue); + String result = getFromContentProvider(mContentResolver, sNamespace, sKey); + assertEquals(sValue, result); + result = getFromContentProvider(mContentResolver, newNamespace, sKey); + assertEquals(sValue, result); + + try { + executeShellCommand("device_config delete " + sNamespace + " " + sKey); + // sKey is deleted from sNamespace + result = getFromContentProvider(mContentResolver, sNamespace, sKey); + assertNull(result); + // sKey is not deleted from newNamespace + result = getFromContentProvider(mContentResolver, newNamespace, sKey); + assertEquals(sValue, result); + } finally { + deleteFromContentProvider(mContentResolver, newNamespace, sKey); + } + } + + @Test + public void testDelete_invalidArgs() throws Exception { + putWithContentProvider(mContentResolver, sNamespace, sKey, sValue); + String result = getFromContentProvider(mContentResolver, sNamespace, sKey); + assertEquals(sValue, result); + + // missing sNamespace + executeShellCommand("device_config delete " + sKey); + result = getFromContentProvider(mContentResolver, sNamespace, sKey); + // sValue was not deleted + assertEquals(sValue, result); + + // too many arguments + executeShellCommand("device_config delete " + sNamespace + " " + sKey + " extra_arg"); + result = getFromContentProvider(mContentResolver, sNamespace, sKey); + // sValue was not deleted + assertEquals(sValue, result); + } + + @Test + public void testReset_setUntrustedDefault() throws Exception { + String newValue = "value2"; + + // make sValue the untrusted default (set by root) + executeShellCommand( + "device_config put " + sNamespace + " " + sKey + " " + sValue + " default"); + // make newValue the current value + executeShellCommand( + "device_config put " + sNamespace + " " + sKey + " " + newValue); + String result = getFromContentProvider(mContentResolver, sNamespace, sKey); + assertEquals(newValue, result); + + executeShellCommand("device_config reset untrusted_defaults " + sNamespace); + result = getFromContentProvider(mContentResolver, sNamespace, sKey); + // back to the default + assertEquals(sValue, result); + + executeShellCommand("device_config reset trusted_defaults " + sNamespace); + result = getFromContentProvider(mContentResolver, sNamespace, sKey); + // not trusted default was set + assertNull(result); + } + + @Test + public void testReset_setTrustedDefault() throws Exception { + String newValue = "value2"; + + // make sValue the trusted default (set by system) + putWithContentProvider(mContentResolver, sNamespace, sKey, sValue, true); + // make newValue the current value + executeShellCommand( + "device_config put " + sNamespace + " " + sKey + " " + newValue); + String result = getFromContentProvider(mContentResolver, sNamespace, sKey); + assertEquals(newValue, result); + + executeShellCommand("device_config reset untrusted_defaults " + sNamespace); + result = getFromContentProvider(mContentResolver, sNamespace, sKey); + // back to the default + assertEquals(sValue, result); + + executeShellCommand("device_config reset trusted_defaults " + sNamespace); + result = getFromContentProvider(mContentResolver, sNamespace, sKey); + // our trusted default is still set + assertEquals(sValue, result); + } + + private static void executeShellCommand(String command) throws IOException { + InputStream is = new FileInputStream(InstrumentationRegistry.getInstrumentation() + .getUiAutomation().executeShellCommand(command).getFileDescriptor()); + Streams.readFully(is); + } + + private static void putWithContentProvider(ContentResolver resolver, String namespace, + String key, String value) { + putWithContentProvider(resolver, namespace, key, value, false); + } + + private static void putWithContentProvider(ContentResolver resolver, String namespace, + String key, String value, boolean makeDefault) { + String compositeName = namespace + "/" + key; + Bundle args = new Bundle(); + args.putString(Settings.NameValueTable.VALUE, value); + if (makeDefault) { + args.putBoolean(Settings.CALL_METHOD_MAKE_DEFAULT_KEY, true); + } + resolver.call( + CONFIG_CONTENT_URI, Settings.CALL_METHOD_PUT_CONFIG, compositeName, args); + } + + private static String getFromContentProvider(ContentResolver resolver, String namespace, + String key) { + String compositeName = namespace + "/" + key; + Bundle result = resolver.call( + CONFIG_CONTENT_URI, Settings.CALL_METHOD_GET_CONFIG, compositeName, null); + assertNotNull(result); + return result.getString(Settings.NameValueTable.VALUE); + } + + private static boolean deleteFromContentProvider(ContentResolver resolver, String namespace, + String key) { + String compositeName = namespace + "/" + key; + Bundle result = resolver.call( + CONFIG_CONTENT_URI, Settings.CALL_METHOD_DELETE_CONFIG, compositeName, null); + assertNotNull(result); + return compositeName.equals(result.getString(Settings.NameValueTable.VALUE)); + } +} |