Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4bcc707b authored by Matt Pape's avatar Matt Pape Committed by Android (Google) Code Review
Browse files

Merge "Add binder implementation to support device config shell commands from the command line."

parents c8ca43ac abb6789b
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
# 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)
+2 −0
Original line number Diff line number Diff line
#!/system/bin/sh
cmd device_config "$@"
+355 −0
Original line number Diff line number Diff line
/*
 * 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;
                }
            }
        }
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -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;
    }

+236 −0
Original line number Diff line number Diff line
/*
 * 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));
    }
}