Java程序  |  466行  |  19.2 KB

/*
 * Copyright (C) 2019 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.server.testharness;

import android.annotation.Nullable;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.UserInfo;
import android.debug.AdbManagerInternal;
import android.location.LocationManager;
import android.os.BatteryManager;
import android.os.Binder;
import android.os.IBinder;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.ShellCommand;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.util.Slog;

import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.widget.LockPatternUtils;
import com.android.server.LocalServices;
import com.android.server.PersistentDataBlockManagerInternal;
import com.android.server.SystemService;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Set;

/**
 * Manages the Test Harness Mode service for setting up test harness mode on the device.
 *
 * <p>Test Harness Mode is a feature that allows the user to clean their device, retain ADB keys,
 * and provision the device for Instrumentation testing. This means that all parts of the device
 * that would otherwise interfere with testing (auto-syncing accounts, package verification,
 * automatic updates, etc.) are all disabled by default but may be re-enabled by the user.
 */
public class TestHarnessModeService extends SystemService {
    private static final String TAG = TestHarnessModeService.class.getSimpleName();
    private static final String TEST_HARNESS_MODE_PROPERTY = "persist.sys.test_harness";

    private PersistentDataBlockManagerInternal mPersistentDataBlockManagerInternal;

    public TestHarnessModeService(Context context) {
        super(context);
    }

    @Override
    public void onStart() {
        publishBinderService("testharness", mService);
    }

    @Override
    public void onBootPhase(int phase) {
        switch (phase) {
            case PHASE_SYSTEM_SERVICES_READY:
                setUpTestHarnessMode();
                break;
            case PHASE_BOOT_COMPLETED:
                completeTestHarnessModeSetup();
                showNotificationIfEnabled();
                break;
        }
        super.onBootPhase(phase);
    }

    /**
     * Begin the setup for Test Harness Mode.
     *
     * <p>Note: This is just the things that <em>need</em> to be done before the device finishes
     * booting for the first time. Everything else should be done after the system is done booting.
     */
    private void setUpTestHarnessMode() {
        Slog.d(TAG, "Setting up test harness mode");
        byte[] testHarnessModeData = getTestHarnessModeData();
        if (testHarnessModeData == null) {
            return;
        }
        // If there is data, we should set the device as provisioned, so that we skip the setup
        // wizard.
        setDeviceProvisioned();
        disableLockScreen();
        SystemProperties.set(TEST_HARNESS_MODE_PROPERTY, "1");
    }

    private void disableLockScreen() {
        UserInfo userInfo = getPrimaryUser();
        LockPatternUtils utils = new LockPatternUtils(getContext());
        utils.setLockScreenDisabled(true, userInfo.id);
    }

    private void completeTestHarnessModeSetup() {
        Slog.d(TAG, "Completing Test Harness Mode setup.");
        byte[] testHarnessModeData = getTestHarnessModeData();
        if (testHarnessModeData == null) {
            return;
        }
        try {
            setUpAdbFiles(PersistentData.fromBytes(testHarnessModeData));
            configureSettings();
            configureUser();
        } catch (SetUpTestHarnessModeException e) {
            Slog.e(TAG, "Failed to set up Test Harness Mode. Bad data.", e);
        } finally {
            // Clear out the Test Harness Mode data so that we don't repeat the setup. If it failed
            // to set up, then retrying without enabling Test Harness Mode should allow it to boot.
            // If we succeeded setting up, we shouldn't be re-applying the THM steps every boot
            // anyway.
            getPersistentDataBlock().clearTestHarnessModeData();
        }
    }

    private byte[] getTestHarnessModeData() {
        PersistentDataBlockManagerInternal blockManager = getPersistentDataBlock();
        if (blockManager == null) {
            Slog.e(TAG, "Failed to start Test Harness Mode; no implementation of "
                    + "PersistentDataBlockManagerInternal was bound!");
            return null;
        }
        byte[] testHarnessModeData = blockManager.getTestHarnessModeData();
        if (testHarnessModeData == null || testHarnessModeData.length == 0) {
            // There's no data to apply, so leave it as-is.
            return null;
        }
        return testHarnessModeData;
    }

    private void configureSettings() {
        ContentResolver cr = getContext().getContentResolver();

        // Disable the TTL for ADB keys before enabling ADB
        Settings.Global.putLong(cr, Settings.Global.ADB_ALLOWED_CONNECTION_TIME, 0);
        Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 1);
        Settings.Global.putInt(cr, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 1);
        Settings.Global.putInt(cr, Settings.Global.PACKAGE_VERIFIER_ENABLE, 0);
        Settings.Global.putInt(
                cr,
                Settings.Global.STAY_ON_WHILE_PLUGGED_IN,
                BatteryManager.BATTERY_PLUGGED_ANY);
        Settings.Global.putInt(cr, Settings.Global.OTA_DISABLE_AUTOMATIC_UPDATE, 1);
    }

    private void setUpAdbFiles(PersistentData persistentData) {
        AdbManagerInternal adbManager = LocalServices.getService(AdbManagerInternal.class);

        writeBytesToFile(persistentData.mAdbKeys, adbManager.getAdbKeysFile().toPath());
        writeBytesToFile(persistentData.mAdbTempKeys, adbManager.getAdbTempKeysFile().toPath());
    }

    private void configureUser() {
        UserInfo primaryUser = getPrimaryUser();

        ContentResolver.setMasterSyncAutomaticallyAsUser(false, primaryUser.id);

        LocationManager locationManager = getContext().getSystemService(LocationManager.class);
        locationManager.setLocationEnabledForUser(true, primaryUser.getUserHandle());
    }

    private UserInfo getPrimaryUser() {
        UserManager userManager = UserManager.get(getContext());
        return userManager.getPrimaryUser();
    }

    private void writeBytesToFile(byte[] keys, Path adbKeys) {
        try {
            OutputStream fileOutputStream = Files.newOutputStream(adbKeys);
            fileOutputStream.write(keys);
            fileOutputStream.close();

            Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(adbKeys);
            permissions.add(PosixFilePermission.GROUP_READ);
            Files.setPosixFilePermissions(adbKeys, permissions);
        } catch (IOException e) {
            Slog.e(TAG, "Failed to set up adb keys", e);
            // Note: if a device enters this block, it will remain UNAUTHORIZED in ADB, but all
            // other settings will be set up.
        }
    }

    // Setting the device as provisioned skips the setup wizard.
    private void setDeviceProvisioned() {
        ContentResolver cr = getContext().getContentResolver();
        Settings.Global.putInt(cr, Settings.Global.DEVICE_PROVISIONED, 1);
        Settings.Secure.putIntForUser(
                cr,
                Settings.Secure.USER_SETUP_COMPLETE,
                1,
                UserHandle.USER_CURRENT);
    }

    private void showNotificationIfEnabled() {
        if (!SystemProperties.getBoolean(TEST_HARNESS_MODE_PROPERTY, false)) {
            return;
        }
        String title = getContext()
                .getString(com.android.internal.R.string.test_harness_mode_notification_title);
        String message = getContext()
                .getString(com.android.internal.R.string.test_harness_mode_notification_message);

        Notification notification =
                new Notification.Builder(getContext(), SystemNotificationChannels.DEVELOPER)
                        .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
                        .setWhen(0)
                        .setOngoing(true)
                        .setTicker(title)
                        .setDefaults(0)  // please be quiet
                        .setColor(getContext().getColor(
                                com.android.internal.R.color
                                        .system_notification_accent_color))
                        .setContentTitle(title)
                        .setContentText(message)
                        .setVisibility(Notification.VISIBILITY_PUBLIC)
                        .build();

        NotificationManager notificationManager =
                getContext().getSystemService(NotificationManager.class);
        notificationManager.notifyAsUser(
                null, SystemMessage.NOTE_TEST_HARNESS_MODE_ENABLED, notification, UserHandle.ALL);
    }

    @Nullable
    private PersistentDataBlockManagerInternal getPersistentDataBlock() {
        if (mPersistentDataBlockManagerInternal == null) {
            Slog.d(TAG, "Getting PersistentDataBlockManagerInternal from LocalServices");
            mPersistentDataBlockManagerInternal =
                    LocalServices.getService(PersistentDataBlockManagerInternal.class);
        }
        return mPersistentDataBlockManagerInternal;
    }

    private final IBinder mService = new Binder() {
        @Override
        public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
                String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
            (new TestHarnessModeShellCommand())
                .exec(this, in, out, err, args, callback, resultReceiver);
        }
    };

    private class TestHarnessModeShellCommand extends ShellCommand {
        @Override
        public int onCommand(String cmd) {
            switch (cmd) {
                case "enable":
                case "restore":
                    checkPermissions();
                    final long originalId = Binder.clearCallingIdentity();
                    try {
                        if (isDeviceSecure()) {
                            getErrPrintWriter().println(
                                    "Test Harness Mode cannot be enabled if there is a lock "
                                            + "screen");
                            return 2;
                        }
                        return handleEnable();
                    } finally {
                        Binder.restoreCallingIdentity(originalId);
                    }
                default:
                    return handleDefaultCommands(cmd);
            }
        }

        private void checkPermissions() {
            getContext().enforceCallingPermission(
                    android.Manifest.permission.ENABLE_TEST_HARNESS_MODE,
                    "You must hold android.permission.ENABLE_TEST_HARNESS_MODE "
                            + "to enable Test Harness Mode");
        }

        private boolean isDeviceSecure() {
            KeyguardManager keyguardManager = getContext().getSystemService(KeyguardManager.class);
            return keyguardManager.isDeviceSecure(getPrimaryUser().id);
        }

        private int handleEnable() {
            AdbManagerInternal adbManager = LocalServices.getService(AdbManagerInternal.class);
            File adbKeys = adbManager.getAdbKeysFile();
            File adbTempKeys = adbManager.getAdbTempKeysFile();
            if (adbKeys == null && adbTempKeys == null) {
                // This should only be accessible on eng builds that haven't yet set up ADB keys
                getErrPrintWriter()
                    .println("No ADB keys stored; not enabling test harness mode");
                return 1;
            }

            try {
                byte[] adbKeysBytes = getBytesFromFile(adbKeys);
                byte[] adbTempKeysBytes = getBytesFromFile(adbTempKeys);

                PersistentData persistentData = new PersistentData(adbKeysBytes, adbTempKeysBytes);
                PersistentDataBlockManagerInternal blockManager = getPersistentDataBlock();
                if (blockManager == null) {
                    Slog.e(TAG, "Failed to enable Test Harness Mode. No implementation of "
                            + "PersistentDataBlockManagerInternal was bound.");
                    getErrPrintWriter().println("Failed to enable Test Harness Mode");
                    return 1;
                }
                blockManager.setTestHarnessModeData(persistentData.toBytes());
            } catch (IOException e) {
                Slog.e(TAG, "Failed to store ADB keys.", e);
                getErrPrintWriter().println("Failed to enable Test Harness Mode");
                return 1;
            }

            Intent i = new Intent(Intent.ACTION_FACTORY_RESET);
            i.setPackage("android");
            i.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
            i.putExtra(Intent.EXTRA_REASON, TAG);
            i.putExtra(Intent.EXTRA_WIPE_EXTERNAL_STORAGE, true);
            getContext().sendBroadcastAsUser(i, UserHandle.SYSTEM);
            return 0;
        }

        private byte[] getBytesFromFile(File file) throws IOException {
            if (file == null || !file.exists()) {
                return new byte[0];
            }
            Path path = file.toPath();
            try (InputStream inputStream = Files.newInputStream(path)) {
                int size = (int) Files.size(path);
                byte[] bytes = new byte[size];
                int numBytes = inputStream.read(bytes);
                if (numBytes != size) {
                    throw new IOException("Failed to read the whole file");
                }
                return bytes;
            }
        }

        @Override
        public void onHelp() {
            PrintWriter pw = getOutPrintWriter();
            pw.println("About:");
            pw.println("  Test Harness Mode is a mode that the device can be placed in to prepare");
            pw.println("  the device for running UI tests. The device is placed into this mode by");
            pw.println("  first wiping all data from the device, preserving ADB keys.");
            pw.println();
            pw.println("  By default, the following settings are configured:");
            pw.println("    * Package Verifier is disabled");
            pw.println("    * Stay Awake While Charging is enabled");
            pw.println("    * OTA Updates are disabled");
            pw.println("    * Auto-Sync for accounts is disabled");
            pw.println();
            pw.println("  Other apps may configure themselves differently in Test Harness Mode by");
            pw.println("  checking ActivityManager.isRunningInUserTestHarness()");
            pw.println();
            pw.println("Test Harness Mode commands:");
            pw.println("  help");
            pw.println("    Print this help text.");
            pw.println();
            pw.println("  enable|restore");
            pw.println("    Erase all data from this device and enable Test Harness Mode,");
            pw.println("    preserving the stored ADB keys currently on the device and toggling");
            pw.println("    settings in a way that are conducive to Instrumentation testing.");
        }
    }

    /**
     * The object that will serialize/deserialize the Test Harness Mode data to and from the
     * persistent data block.
     */
    public static class PersistentData {
        static final byte VERSION_1 = 1;
        static final byte VERSION_2 = 2;

        final int mVersion;
        final byte[] mAdbKeys;
        final byte[] mAdbTempKeys;

        PersistentData(byte[] adbKeys, byte[] adbTempKeys) {
            this(VERSION_2, adbKeys, adbTempKeys);
        }

        PersistentData(int version, byte[] adbKeys, byte[] adbTempKeys) {
            this.mVersion = version;
            this.mAdbKeys = adbKeys;
            this.mAdbTempKeys = adbTempKeys;
        }

        static PersistentData fromBytes(byte[] bytes) throws SetUpTestHarnessModeException {
            try {
                DataInputStream is = new DataInputStream(new ByteArrayInputStream(bytes));
                int version = is.readInt();
                if (version == VERSION_1) {
                    // Version 1 of Test Harness Mode contained an "enabled" bit that we need to
                    // skip. If we don't, the binary format will be bad and it will fail to set up.
                    is.readBoolean();
                }
                int adbKeysLength = is.readInt();
                byte[] adbKeys = new byte[adbKeysLength];
                is.readFully(adbKeys);
                int adbTempKeysLength = is.readInt();
                byte[] adbTempKeys = new byte[adbTempKeysLength];
                is.readFully(adbTempKeys);
                return new PersistentData(version, adbKeys, adbTempKeys);
            } catch (IOException e) {
                throw new SetUpTestHarnessModeException(e);
            }
        }

        byte[] toBytes() {
            try {
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                DataOutputStream dos = new DataOutputStream(os);
                dos.writeInt(VERSION_2);
                dos.writeInt(mAdbKeys.length);
                dos.write(mAdbKeys);
                dos.writeInt(mAdbTempKeys.length);
                dos.write(mAdbTempKeys);
                dos.close();
                return os.toByteArray();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * An exception thrown when Test Harness Mode fails to set up.
     *
     * <p>In the event that Test Harness Mode fails to set up, all of the data should be discarded
     * and the Test Harness Mode portion of the persistent data block should be wiped. This will
     * prevent the device from becoming stuck, as there is no way (without rooting the device) to
     * clear the persistent data block.
     */
    private static class SetUpTestHarnessModeException extends Exception {
        SetUpTestHarnessModeException(Exception e) {
            super(e);
        }
    }
}