blob: 31335fb336884a98dc0caa8eca66defdcd3e1883 [file] [log] [blame]
/*
* Copyright (C) 2020 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.car.provision;
import static android.app.Activity.RESULT_CANCELED;
import static android.app.Activity.RESULT_FIRST_USER;
import static android.app.Activity.RESULT_OK;
import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE;
import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME;
import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION;
import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM;
import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_TRIGGER;
import static android.app.admin.DevicePolicyManager.PROVISIONING_TRIGGER_QR_CODE;
import static android.car.settings.CarSettings.Secure.KEY_ENABLE_INITIAL_NOTICE_SCREEN_TO_USER;
import static android.car.settings.CarSettings.Secure.KEY_SETUP_WIZARD_IN_PROGRESS;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.TextView;
import com.android.car.setupwizardlib.util.CarDrivingStateMonitor;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
/**
* Reference implementeation for a Car SetupWizard.
*
* <p>Features:
*
* <ul>
* <li>Shows UI where user can confirm setup.
* <li>Listen to UX restriction events, so it exits setup when the car moves.
* <li>Add option to setup managed-provisioning mode.
* <li>Sets car-specific properties.
* </ul>
*
* <p>By default, it doesn't show the UI, unless the {@code persist.dev.car_provision.show_ui}
* property is set to {@code true}. For example, you can change it by running something like:
<pre><code>
adb root
adb shell setprop persist.dev.car_provision.show_ui true && \
adb shell pm enable --user cur com.android.car.provision/.DefaultActivity &&\
adb shell settings put secure --user cur user_setup_complete 0 && \
adb shell settings put secure --user 0 user_setup_complete 0 &&\
adb shell settings put global device_provisioned 0 &&\
adb shell rm -f /data/system/device_policies_version &&\
adb shell rm -f /data/system/device_policies.xml &&\
adb shell rm -f /data/system/device_owner_2.xml ;\
adb shell rm -f /data/system/users/`adb shell am get-current-user`/profile_owner.xml
adb shell stop && adb shell start
<code></pre>
*/
public final class DefaultActivity extends Activity {
static final String TAG = "CarProvision";
// TODO(b/170333009): copied from ManagedProvisioning app, as they're hidden;
private static final String PROVISION_FINALIZATION_INSIDE_SUW =
"android.app.action.PROVISION_FINALIZATION_INSIDE_SUW";
private static final int RESULT_CODE_PROFILE_OWNER_SET = 122;
private static final int RESULT_CODE_DEVICE_OWNER_SET = 123;
private static final int REQUEST_CODE_STEP1 = 42;
private static final int REQUEST_CODE_STEP2_PO = 43;
private static final int REQUEST_CODE_STEP2_DO = 44;
private static final int NOTIFICATION_ID = 108;
private static final String IMPORTANCE_DEFAULT_ID = "importance_default";
private static final List<DpcInfo> sSupportedDpcApps = new ArrayList<>(2);
private static final String TEST_DPC_NAME = "TestDPC (downloadable)";
private static final String TEST_DPC_PACKAGE = "com.afwsamples.testdpc";
private static final String TEST_DPC_LEGACY_ACTIVITY = TEST_DPC_PACKAGE
+ ".SetupManagementLaunchActivity";
private static final String TEST_DPC_RECEIVER = TEST_DPC_PACKAGE
+ ".DeviceAdminReceiver";
private static final String LOCAL_TEST_DPC_NAME = "TestDPC (local only)";
private static final String SHOW_UI_SYSTEM_PROPERTY = "persist.dev.car_provision.show_ui";
static {
DpcInfo testDpc = new DpcInfo(TEST_DPC_NAME,
TEST_DPC_PACKAGE,
TEST_DPC_LEGACY_ACTIVITY,
TEST_DPC_RECEIVER,
"gJD2YwtOiWJHkSMkkIfLRlj-quNqG1fb6v100QmzM9w=",
"https://testdpc-latest-apk.appspot.com/preview");
// Locally-built version of the TestDPC
DpcInfo localTestDpc = new DpcInfo(LOCAL_TEST_DPC_NAME,
TEST_DPC_PACKAGE,
TEST_DPC_LEGACY_ACTIVITY,
TEST_DPC_RECEIVER,
/* checkSum= */ null,
/* downloadUrl = */ null);
sSupportedDpcApps.add(testDpc);
sSupportedDpcApps.add(localTestDpc);
}
private CarDrivingStateMonitor mCarDrivingStateMonitor;
private TextView mErrorsTextView;
private Button mFinishSetupButton;
private Button mFactoryResetButton;
private Spinner mDpcAppsSpinner;
private Button mLegacyProvisioningWorkflowButton;
private Button mProvisioningWorkflowButton;
private final BroadcastReceiver mDrivingStateExitReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive(): " + intent);
exitSetup();
}
};
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
int userId = getUserId();
Log.i(TAG, "onCreate() for user " + userId + " Intent: " + getIntent());
if (userId == UserHandle.USER_SYSTEM && UserManager.isHeadlessSystemUserMode()) {
// System user will be provisioned together with the first non-system user
Log.i(TAG, "onCreate(): skipping setup on headless system user");
disableSelfAndFinish();
return;
}
if (!showUi()) {
Log.w(TAG, "onCreate(): skipping UI because " + SHOW_UI_SYSTEM_PROPERTY
+ " was not set to true");
finishSetup();
return;
}
DevicePolicyManager dpm = getSystemService(DevicePolicyManager.class);
if (dpm.isDeviceManaged()) {
Log.i(TAG, "onCreate(): skipping UI on managed device");
finishSetup();
return;
}
setCarSetupInProgress(true);
setContentView(R.layout.default_activity);
mErrorsTextView = findViewById(R.id.error_message);
mFinishSetupButton = findViewById(R.id.finish_setup);
mFactoryResetButton = findViewById(R.id.factory_reset);
mDpcAppsSpinner = findViewById(R.id.dpc_apps);
mLegacyProvisioningWorkflowButton = findViewById(R.id.legacy_provision_workflow);
mProvisioningWorkflowButton = findViewById(R.id.provision_workflow);
mLegacyProvisioningWorkflowButton
.setOnClickListener((v) -> launchLegacyProvisioningWorkflow());
mProvisioningWorkflowButton.setOnClickListener((v) -> launchProvisioningWorkflow());
mFinishSetupButton.setOnClickListener((v) -> finishSetup());
mFactoryResetButton.setOnClickListener((v) -> factoryReset());
updateUi();
setManagedProvisioning(dpm);
startMonitor();
}
private boolean showUi() {
boolean result = false;
try {
result = SystemProperties.getBoolean(SHOW_UI_SYSTEM_PROPERTY, false);
} catch (Exception e) {
Log.w(TAG, "error getting property " + SHOW_UI_SYSTEM_PROPERTY);
}
return result;
}
private void startMonitor() {
Log.d(TAG, "startMonitor()");
registerReceiver(mDrivingStateExitReceiver,
new IntentFilter(CarDrivingStateMonitor.EXIT_BROADCAST_ACTION));
mCarDrivingStateMonitor = CarDrivingStateMonitor.get(this);
mCarDrivingStateMonitor.startMonitor();
}
@Override
public void finish() {
Log.i(TAG, "finish() for user " + getUserId());
stopMonitor();
super.finish();
};
@Override
public void dump(String prefix, FileDescriptor fd, PrintWriter pw, String[] args) {
if (args == null || args.length == 0) {
showDpcs(pw);
showHelp(pw);
return;
}
if (args[0].equals("--help")) {
showHelp(pw);
return;
}
addDpc(pw, args);
};
private void showDpcs(PrintWriter pw) {
pw.printf("%d DPCs\n", sSupportedDpcApps.size());
sSupportedDpcApps.forEach((dpc) -> pw.printf("\t%s\n", dpc));
}
private void showHelp(PrintWriter pw) {
pw.println("\nTo add a new DPC, use: --name name --package-name package-name"
+ "--receiver-name receiver-name [--legacy-activity-name legacy-activity-name] "
+ "[--checksum checksum] [--download-url download-url]");
}
private void addDpc(PrintWriter pw, String[] args) {
String name = null;
String packageName = null;
String legacyActivityName = null;
String receiverName = null;
String checkSum = null;
String downloadUrl = null;
for (int i = 0; i < args.length; i++) {
try {
switch (args[i]) {
case "--name":
name = args[++i];
break;
case "--package-name":
packageName = args[++i];
break;
case "--legacy-activity-name":
legacyActivityName = args[++i];
break;
case "--receiver-name":
receiverName = args[++i];
break;
case "--checksum":
checkSum = args[++i];
break;
case "--download-url":
downloadUrl = args[++i];
break;
default:
pw.printf("Invalid option at index %d: %s\n", i, args[i]);
return;
}
} catch (Exception e) {
// most likely a missing arg...
pw.printf("Error handing arg %d: %s\n", i, e);
return;
}
}
DpcInfo dpc = new DpcInfo(name, packageName, legacyActivityName, receiverName, checkSum,
downloadUrl);
Log.i(TAG, "Adding new DPC from dump(): " + dpc);
sSupportedDpcApps.add(dpc);
pw.printf("Added new DPC: %s\n", dpc);
updateUi();
}
private void stopMonitor() {
Log.d(TAG, "stopMonitor()");
if (mCarDrivingStateMonitor == null) {
// Happens when device is managed (and startMonitor() is skipped)
Log.d(TAG, "Already stopped (or never stopped)");
return;
}
if (mDrivingStateExitReceiver != null) {
unregisterReceiver(mDrivingStateExitReceiver);
}
mCarDrivingStateMonitor.stopMonitor();
mCarDrivingStateMonitor = null;
}
private void updateUi() {
String[] appNames = new String[sSupportedDpcApps.size()];
for (int i = 0; i < sSupportedDpcApps.size(); i++) {
appNames[i] = sSupportedDpcApps.get(i).name;
}
mDpcAppsSpinner.setAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_item, appNames));
mDpcAppsSpinner.setSelection(appNames.length - 1);
}
private void setManagedProvisioning(DevicePolicyManager dpm) {
if (!getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)) {
Log.i(TAG, "Disabling provisioning buttons because device does not have the "
+ PackageManager.FEATURE_DEVICE_ADMIN + " feature");
return;
}
if (!dpm.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE)) {
Log.w(TAG, "Disabling provisioning buttons because device cannot be provisioned - "
+ "it can only be set on first boot");
return;
}
mProvisioningWorkflowButton.setEnabled(true);
mLegacyProvisioningWorkflowButton.setEnabled(true);
}
private boolean checkDpcAppExists(String dpcApp) {
if (!checkAppExists(dpcApp, UserHandle.USER_SYSTEM)) return false;
if (!checkAppExists(dpcApp, getUserId())) return false;
return true;
}
private boolean checkAppExists(String app, int userId) {
Log.d(TAG, "Checking if " + app + " exits for user " + userId);
try {
PackageInfo info = getPackageManager().getPackageInfoAsUser(app, /* flags= */ 0,
userId);
if (info == null) {
Log.i(TAG, "No app " + app + " for user " + userId);
return false;
}
Log.d(TAG, "Found it: " + info);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
} catch (Exception e) {
Log.e(TAG, "Error checking if " + app + " exists for user " + userId, e);
return false;
}
}
private void finishSetup() {
Log.i(TAG, "finishing setup for user " + getUserId());
provisionUserAndDevice();
disableSelfAndFinish();
}
private void factoryReset() {
new AlertDialog.Builder(this).setMessage(R.string.factory_reset_warning)
.setPositiveButton(android.R.string.ok, (d, w)->sendFactoryResetIntent())
.show();
}
private void sendFactoryResetIntent() {
provisionUserAndDevice();
Intent intent = new Intent(Intent.ACTION_FACTORY_RESET);
intent.setPackage("android");
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.putExtra(Intent.EXTRA_REASON, "Requested by user on SUW");
Log.i(TAG, "factory resetting device with intent " + intent);
sendBroadcast(intent);
disableSelfAndFinish();
}
private void provisionUserAndDevice() {
Log.d(TAG, "setting Settings properties");
// Add a persistent setting to allow other apps to know the device has been provisioned.
if (!isDeviceProvisioned()) {
Settings.Global.putInt(getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 1);
}
maybeMarkSystemUserSetupComplete();
Log.v(TAG, "Marking USER_SETUP_COMPLETE for user " + getUserId());
markUserSetupComplete(this);
// Set car-specific properties
setCarSetupInProgress(false);
Settings.Secure.putInt(getContentResolver(), KEY_ENABLE_INITIAL_NOTICE_SCREEN_TO_USER, 0);
}
private boolean isDeviceProvisioned() {
try {
return Settings.Global.getInt(getContentResolver(),
Settings.Global.DEVICE_PROVISIONED) == 1;
} catch (SettingNotFoundException e) {
Log.wtf(TAG, "DEVICE_PROVISIONED is not found.");
return false;
}
}
private boolean isUserSetupComplete(Context context) {
return Settings.Secure.getInt(context.getContentResolver(),
Settings.Secure.USER_SETUP_COMPLETE, /* default= */ 0) == 1;
}
private void maybeMarkSystemUserSetupComplete() {
Context systemUserContext = getApplicationContext().createContextAsUser(
UserHandle.SYSTEM, /* flags= */ 0);
if (!isUserSetupComplete(systemUserContext) && getUserId() != UserHandle.USER_SYSTEM
&& UserManager.isHeadlessSystemUserMode()) {
Log.v(TAG, "Marking USER_SETUP_COMPLETE for system user");
markUserSetupComplete(systemUserContext);
}
}
private void setCarSetupInProgress(boolean inProgress) {
Settings.Secure.putInt(getContentResolver(), KEY_SETUP_WIZARD_IN_PROGRESS,
inProgress ? 1 : 0);
}
private void markUserSetupComplete(Context context) {
Settings.Secure.putInt(context.getContentResolver(),
Settings.Secure.USER_SETUP_COMPLETE, 1);
}
private void exitSetup() {
Log.d(TAG, "exiting setup early for user " + getUserId());
provisionUserAndDevice();
notifySetupExited();
disableSelfAndFinish();
}
private void notifySetupExited() {
Log.d(TAG, "Sending exited setup notification");
NotificationManager notificationMgr = getSystemService(NotificationManager.class);
notificationMgr.createNotificationChannel(new NotificationChannel(
IMPORTANCE_DEFAULT_ID, "Importance Default",
NotificationManager.IMPORTANCE_DEFAULT));
Notification notification = new Notification
.Builder(this, IMPORTANCE_DEFAULT_ID)
.setContentTitle(getString(R.string.exited_setup_title))
.setContentText(getString(R.string.exited_setup_content))
.setCategory(Notification.CATEGORY_CAR_INFORMATION)
.setSmallIcon(R.drawable.car_ic_mode)
.build();
notificationMgr.notify(NOTIFICATION_ID, notification);
}
private DpcInfo getSelectedDpcInfo() {
return sSupportedDpcApps.get(mDpcAppsSpinner.getSelectedItemPosition());
}
private void launchLegacyProvisioningWorkflow() {
DpcInfo dpcInfo = getSelectedDpcInfo();
if (!checkDpcAppExists(dpcInfo.packageName)) {
showErrorMessage("Cannot provision device because " + dpcInfo.packageName
+ " is not available.\n Make sure it's installed for both user 0 and user "
+ getUserId());
return;
}
Intent intent = new Intent();
intent.setComponent(dpcInfo.getLegacyActivityComponentName());
Log.i(TAG, "Provisioning device using LEGACY workflow while running as user "
+ getUserId() + ". DPC: " + dpcInfo + ". Intent: " + intent);
startActivityForResult(intent, REQUEST_CODE_STEP1);
}
private void launchProvisioningWorkflow() {
DpcInfo dpcInfo = getSelectedDpcInfo();
Intent intent = new Intent(ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE);
// TODO(b/170333009): add a UI with options for EXTRA_PROVISIONING_TRIGGER.
intent.putExtra(EXTRA_PROVISIONING_TRIGGER, PROVISIONING_TRIGGER_QR_CODE);
intent.putExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME,
dpcInfo.getAdminReceiverComponentName());
if (dpcInfo.checkSum != null) {
intent.putExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM, dpcInfo.checkSum);
}
if (dpcInfo.downloadUrl != null) {
intent.putExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION,
dpcInfo.downloadUrl);
}
Log.i(TAG, "Provisioning device using NEW workflow while running as user "
+ getUserId() + ". DPC: " + dpcInfo + ". Intent: " + intent);
startActivityForResult(intent, REQUEST_CODE_STEP1);
}
private void disableSelfAndFinish() {
Log.d(TAG, "disableSelfAndFinish()");
// Remove this activity from the package manager.
PackageManager pm = getPackageManager();
ComponentName name = new ComponentName(this, DefaultActivity.class);
Log.i(TAG, "Disabling itself (" + name + ") for user " + getUserId());
pm.setComponentEnabledSetting(name, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
finish();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.d(TAG, "onActivityResult(): request=" + requestCode + ", result="
+ resultCodeToString(resultCode) + ", data=" + data);
switch (requestCode) {
case REQUEST_CODE_STEP1:
onProvisioningStep1Result(resultCode);
break;
case REQUEST_CODE_STEP2_PO:
case REQUEST_CODE_STEP2_DO:
onProvisioningStep2Result(requestCode, resultCode);
break;
default:
showErrorMessage("onActivityResult(): invalid request code " + requestCode);
}
}
private void onProvisioningStep1Result(int resultCode) {
int requestCodeStep2;
switch (resultCode) {
case RESULT_CODE_PROFILE_OWNER_SET:
requestCodeStep2 = REQUEST_CODE_STEP2_PO;
break;
case RESULT_CODE_DEVICE_OWNER_SET:
requestCodeStep2 = REQUEST_CODE_STEP2_DO;
break;
default:
showErrorMessage("onProvisioningStep1Result(): invalid result code "
+ resultCodeToString(resultCode)
+ getManagedProvisioningFailureWarning());
return;
}
Intent intent = new Intent(PROVISION_FINALIZATION_INSIDE_SUW)
.addCategory(Intent.CATEGORY_DEFAULT);
Log.i(TAG, "Finalizing DPC with " + intent);
startActivityForResult(intent, requestCodeStep2);
}
private String getManagedProvisioningFailureWarning() {
return "\n\n" + getString(R.string.provision_failure_message);
}
private void onProvisioningStep2Result(int requestCode, int resultCode) {
boolean doMode = requestCode == REQUEST_CODE_STEP2_DO;
if (resultCode != RESULT_OK) {
StringBuilder message = new StringBuilder("onProvisioningStep2Result(): "
+ "invalid result code ").append(resultCode);
if (doMode) {
message.append(getManagedProvisioningFailureWarning());
}
showErrorMessage(message.toString());
return;
}
Log.i(TAG, (doMode ? "Device owner" : "Profile owner") + " mode provisioned!");
finishSetup();
}
private static String resultCodeToString(int resultCode) {
StringBuilder result = new StringBuilder();
switch (resultCode) {
case RESULT_OK:
result.append("RESULT_OK");
break;
case RESULT_CANCELED:
result.append("RESULT_CANCELED");
break;
case RESULT_FIRST_USER:
result.append("RESULT_FIRST_USER");
break;
case RESULT_CODE_PROFILE_OWNER_SET:
result.append("RESULT_CODE_PROFILE_OWNER_SET");
break;
case RESULT_CODE_DEVICE_OWNER_SET:
result.append("RESULT_CODE_DEVICE_OWNER_SET");
break;
default:
result.append("UNKNOWN_CODE");
}
return result.append('(').append(resultCode).append(')').toString();
}
private void showErrorMessage(String message) {
Log.e(TAG, "Error: " + message);
mErrorsTextView.setText(message);
findViewById(R.id.errors_container).setVisibility(View.VISIBLE);
}
}