| /* |
| * 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.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.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; |
| private boolean mShouldSetUpTestHarnessMode; |
| |
| 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: |
| disableAutoSync(); |
| configureSettings(); |
| showNotification(); |
| break; |
| } |
| super.onBootPhase(phase); |
| } |
| |
| private void setUpTestHarnessMode() { |
| Slog.d(TAG, "Setting up test harness mode"); |
| byte[] testHarnessModeData = getPersistentDataBlock().getTestHarnessModeData(); |
| if (testHarnessModeData == null || testHarnessModeData.length == 0) { |
| // There's no data to apply, so leave it as-is. |
| return; |
| } |
| PersistentData persistentData; |
| try { |
| persistentData = PersistentData.fromBytes(testHarnessModeData); |
| } catch (SetUpTestHarnessModeException e) { |
| Slog.e(TAG, "Failed to set up Test Harness Mode. Bad data.", e); |
| return; |
| } finally { |
| // Clear out the Test Harness Mode data. It's now in memory if successful or we should |
| // skip setting up. |
| getPersistentDataBlock().clearTestHarnessModeData(); |
| } |
| mShouldSetUpTestHarnessMode = true; |
| setUpAdb(persistentData); |
| setDeviceProvisioned(); |
| } |
| |
| private void setUpAdb(PersistentData persistentData) { |
| ContentResolver cr = getContext().getContentResolver(); |
| |
| // Disable the TTL for ADB keys before enabling ADB |
| Settings.Global.putLong(cr, Settings.Global.ADB_ALLOWED_CONNECTION_TIME, 0); |
| |
| SystemProperties.set(TEST_HARNESS_MODE_PROPERTY, "1"); |
| writeAdbKeysFile(persistentData); |
| } |
| |
| private void disableAutoSync() { |
| if (!mShouldSetUpTestHarnessMode) { |
| return; |
| } |
| UserInfo primaryUser = UserManager.get(getContext()).getPrimaryUser(); |
| ContentResolver |
| .setMasterSyncAutomaticallyAsUser(false, primaryUser.getUserHandle().getIdentifier()); |
| } |
| |
| private void configureSettings() { |
| if (!mShouldSetUpTestHarnessMode) { |
| return; |
| } |
| ContentResolver cr = getContext().getContentResolver(); |
| |
| 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 writeAdbKeysFile(PersistentData persistentData) { |
| AdbManagerInternal adbManager = LocalServices.getService(AdbManagerInternal.class); |
| |
| writeBytesToFile(persistentData.mAdbKeys, adbManager.getAdbKeysFile().toPath()); |
| writeBytesToFile(persistentData.mAdbTempKeys, adbManager.getAdbTempKeysFile().toPath()); |
| } |
| |
| 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 showNotification() { |
| 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() { |
| UserInfo primaryUser = UserManager.get(getContext()).getPrimaryUser(); |
| KeyguardManager keyguardManager = getContext().getSystemService(KeyguardManager.class); |
| return keyguardManager.isDeviceSecure(primaryUser.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); |
| getPersistentDataBlock().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); |
| } |
| } |
| } |