diff --git a/packages/CarSystemUI/AndroidManifest.xml b/packages/CarSystemUI/AndroidManifest.xml
index 195d4fee51627..261b9f508ccd6 100644
--- a/packages/CarSystemUI/AndroidManifest.xml
+++ b/packages/CarSystemUI/AndroidManifest.xml
@@ -21,4 +21,8 @@
coreApp="true">
+
+
+
+
diff --git a/packages/CarSystemUI/res/drawable/unlock_dialog_background.xml b/packages/CarSystemUI/res/drawable/unlock_dialog_background.xml
new file mode 100644
index 0000000000000..bec6ba7b7c4fc
--- /dev/null
+++ b/packages/CarSystemUI/res/drawable/unlock_dialog_background.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/CarSystemUI/res/layout/trust_agent_unlock_dialog.xml b/packages/CarSystemUI/res/layout/trust_agent_unlock_dialog.xml
new file mode 100644
index 0000000000000..2d9901c30d024
--- /dev/null
+++ b/packages/CarSystemUI/res/layout/trust_agent_unlock_dialog.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/CarSystemUI/res/values/colors_car.xml b/packages/CarSystemUI/res/values/colors_car.xml
index 69ab3f3cf957d..0a3f7aa92d846 100644
--- a/packages/CarSystemUI/res/values/colors_car.xml
+++ b/packages/CarSystemUI/res/values/colors_car.xml
@@ -26,4 +26,9 @@
@*android:color/car_dark_blue_grey_600
@*android:color/car_body1_light
+
+ #ff282a2d
+ @*android:color/car_grey_400
+ #ff66b5ff
+
diff --git a/packages/CarSystemUI/res/values/dimens_car.xml b/packages/CarSystemUI/res/values/dimens_car.xml
index 42a764959545a..9cb09c942781f 100644
--- a/packages/CarSystemUI/res/values/dimens_car.xml
+++ b/packages/CarSystemUI/res/values/dimens_car.xml
@@ -43,4 +43,10 @@
-420dp
+
+ 500dp
+ 16dp
+ 100dp
+ 140dp
+
diff --git a/packages/CarSystemUI/res/values/integers_car.xml b/packages/CarSystemUI/res/values/integers_car.xml
index be2cb0d8d9006..862ba751aa550 100644
--- a/packages/CarSystemUI/res/values/integers_car.xml
+++ b/packages/CarSystemUI/res/values/integers_car.xml
@@ -31,5 +31,7 @@
80
+
+ 3000
diff --git a/packages/CarSystemUI/res/values/strings_car.xml b/packages/CarSystemUI/res/values/strings_car.xml
index 83e91c57ccc37..717692e2f02f4 100644
--- a/packages/CarSystemUI/res/values/strings_car.xml
+++ b/packages/CarSystemUI/res/values/strings_car.xml
@@ -29,4 +29,19 @@
When you add a new user, that person needs to set up their space.
Any user can update apps for all other users.
+
+ Waiting\u2026
+
+ Looking for trusted device\u2026
+
+
+ Enter PIN instead
+
+ Enter Pattern instead
+
+ Enter Password instead
+
+ Default Name
+
+ Unlock Dialogue
diff --git a/packages/CarSystemUI/res/values/styles.xml b/packages/CarSystemUI/res/values/styles.xml
index 371bebdebc86a..a9423bf6f2602 100644
--- a/packages/CarSystemUI/res/values/styles.xml
+++ b/packages/CarSystemUI/res/values/styles.xml
@@ -46,4 +46,12 @@
- 96dp
- @drawable/nav_button_background
+
+
\ No newline at end of file
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
index b1f9797086152..97fbea6ea2377 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -26,6 +26,7 @@ import android.car.Car;
import android.car.drivingstate.CarDrivingStateEvent;
import android.car.drivingstate.CarUxRestrictionsManager;
import android.car.hardware.power.CarPowerManager.CarPowerStateListener;
+import android.car.trust.CarTrustAgentEnrollmentManager;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
@@ -957,8 +958,12 @@ public class CarStatusBar extends StatusBar implements
UserSwitcherController userSwitcherController =
Dependency.get(UserSwitcherController.class);
if (userSwitcherController.useFullscreenUserSwitcher()) {
+ Car car = Car.createCar(mContext);
+ CarTrustAgentEnrollmentManager enrollmentManager = (CarTrustAgentEnrollmentManager) car
+ .getCarManager(Car.CAR_TRUST_AGENT_ENROLLMENT_SERVICE);
mFullscreenUserSwitcher = new FullscreenUserSwitcher(this,
- mStatusBarWindow.findViewById(R.id.fullscreen_user_switcher_stub), mContext);
+ mStatusBarWindow.findViewById(R.id.fullscreen_user_switcher_stub),
+ enrollmentManager, mContext);
} else {
super.createUserSwitcher();
}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarTrustAgentUnlockDialogHelper.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarTrustAgentUnlockDialogHelper.java
new file mode 100644
index 0000000000000..78bb1bcf24a87
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarTrustAgentUnlockDialogHelper.java
@@ -0,0 +1,240 @@
+/*
+ * 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.systemui.statusbar.car;
+
+import android.app.admin.DevicePolicyManager;
+import android.bluetooth.BluetoothAdapter;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.internal.widget.LockPatternUtils;
+import com.android.systemui.R;
+
+/**
+ * A helper class displays an unlock dialog and receives broadcast about detecting trusted device
+ * & unlocking state to show the appropriate message on the dialog.
+ */
+class CarTrustAgentUnlockDialogHelper extends BroadcastReceiver{
+ private static final String TAG = CarTrustAgentUnlockDialogHelper.class.getSimpleName();
+
+ private final Context mContext;
+ private final WindowManager mWindowManager;
+ private final UserManager mUserManager;
+ private final WindowManager.LayoutParams mParams;
+ /**
+ * Not using Dialog because context passed from {@link FullscreenUserSwitcher} is not an
+ * activity.
+ */
+ private final View mUnlockDialog;
+ private final TextView mUnlockingText;
+ private final Button mButton;
+ private final IntentFilter mFilter;
+ private int mUid;
+ private boolean mIsDialogShowing;
+ private OnHideListener mOnHideListener;
+
+ CarTrustAgentUnlockDialogHelper(Context context) {
+ mContext = context;
+ mUserManager = mContext.getSystemService(UserManager.class);
+ mWindowManager = mContext.getSystemService(WindowManager.class);
+ mParams = createLayoutParams();
+ mFilter = getIntentFilter();
+
+ mParams.packageName = mContext.getPackageName();
+ mParams.setTitle(mContext.getString(R.string.unlock_dialog_title));
+
+ mUnlockDialog = LayoutInflater.from(mContext).inflate(
+ R.layout.trust_agent_unlock_dialog, null);
+ mUnlockDialog.setLayoutParams(mParams);
+
+ mUnlockingText = mUnlockDialog.findViewById(R.id.unlocking_text);
+ mButton = mUnlockDialog.findViewById(R.id.enter_pin_button);
+ mButton.setOnClickListener(v -> {
+ hideUnlockDialog(/* notifyOnHideListener= */true);
+ // TODO(b/138250105) Stop unlock advertising
+ });
+
+ BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ if (bluetoothAdapter != null
+ && bluetoothAdapter.getLeState() == BluetoothAdapter.STATE_BLE_ON) {
+ mUnlockingText.setText(R.string.unlock_dialog_message_start);
+ }
+ }
+
+ /**
+ * This filter is listening on:
+ * {@link BluetoothAdapter#ACTION_BLE_STATE_CHANGED} for starting unlock advertising;
+ * {@link Intent#ACTION_USER_UNLOCKED} for IHU unlocked
+ */
+ private IntentFilter getIntentFilter() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(BluetoothAdapter.ACTION_BLE_STATE_CHANGED);
+ filter.addAction(Intent.ACTION_USER_UNLOCKED);
+ return filter;
+ }
+
+ /**
+ * Show dialog for the given user
+ */
+ void showUnlockDialog(int uid, OnHideListener listener) {
+ showUnlockDialogAfterDelay(uid, 0, listener);
+ }
+
+ /**
+ * Show dialog for the given user after the certain time of delay has elapsed
+ *
+ * @param uid the user to unlock
+ * @param listener listener that listens to dialog hide
+ */
+ void showUnlockDialogAfterDelay(int uid, OnHideListener listener) {
+ long delayMillis = mContext.getResources().getInteger(R.integer.unlock_dialog_delay_ms);
+ showUnlockDialogAfterDelay(uid, delayMillis, listener);
+ }
+
+ /**
+ * Show dialog for the given user after the supplied delay has elapsed
+ */
+ private void showUnlockDialogAfterDelay(int uid, long delayMillis, OnHideListener listener) {
+ setUid(uid);
+ mOnHideListener = listener;
+ if (!mIsDialogShowing) {
+ logd("Receiver registered");
+ mContext.registerReceiverAsUser(this, UserHandle.ALL, mFilter,
+ /* broadcastPermission= */ null,
+ /* scheduler= */ null);
+ new Handler().postDelayed(() -> {
+ if (!mUserManager.isUserUnlocked(uid)) {
+ logd("Showed unlock dialog for user: " + uid + " after " + delayMillis
+ + " delay.");
+ mWindowManager.addView(mUnlockDialog, mParams);
+ }
+ }, delayMillis);
+ }
+ mIsDialogShowing = true;
+ }
+
+ private void setUid(int uid) {
+ mUid = uid;
+ TextView userName = mUnlockDialog.findViewById(R.id.user_name);
+ userName.setText(mUserManager.getUserInfo(mUid).name);
+ ImageView avatar = mUnlockDialog.findViewById(R.id.avatar);
+ avatar.setImageBitmap(mUserManager.getUserIcon(mUid));
+ setButtonText();
+ }
+
+ private void hideUnlockDialog(boolean notifyOnHideListener) {
+ if (!mIsDialogShowing) {
+ return;
+ }
+ mWindowManager.removeView(mUnlockDialog);
+ logd("Receiver unregistered");
+ mContext.unregisterReceiver(this);
+ if (notifyOnHideListener && mOnHideListener != null) {
+ mOnHideListener.onHide();
+ }
+ mIsDialogShowing = false;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action == null) {
+ return;
+ }
+ switch (action) {
+ case BluetoothAdapter.ACTION_BLE_STATE_CHANGED:
+ logd("Received ACTION_BLE_STATE_CHANGED");
+ int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
+ if (state == BluetoothAdapter.STATE_BLE_ON) {
+ logd("Received BLE_ON");
+ mUnlockingText.setText(R.string.unlock_dialog_message_start);
+ }
+ break;
+ case Intent.ACTION_USER_UNLOCKED:
+ int uid = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ if (uid == mUid) {
+ logd("IHU unlocked");
+ hideUnlockDialog(/* notifyOnHideListener= */false);
+ } else {
+ Log.e(TAG, "Received ACTION_USER_UNLOCKED for unexpected uid: " + uid);
+ }
+ break;
+ default:
+ Log.e(TAG, "Encountered unexpected action when attempting to set "
+ + "unlock state message: " + action);
+ }
+ }
+
+ // Set button text based on security lock type
+ private void setButtonText() {
+ LockPatternUtils lockPatternUtils = new LockPatternUtils(mContext);
+ int passwordQuality = lockPatternUtils.getActivePasswordQuality(mUid);
+ switch (passwordQuality) {
+ // PIN
+ case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX:
+ // Pattern
+ case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING:
+ mButton.setText(R.string.unlock_dialog_button_text_pattern);
+ break;
+ // Password
+ case DevicePolicyManager.PASSWORD_QUALITY_MANAGED:
+ mButton.setText(R.string.unlock_dialog_button_text_password);
+ break;
+ default:
+ Log.e(TAG, "Encountered unexpected security type when attempting to set "
+ + "button text:" + passwordQuality);
+ }
+ }
+
+ private WindowManager.LayoutParams createLayoutParams() {
+ return new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN
+ | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
+ | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION,
+ PixelFormat.TRANSLUCENT
+ );
+ }
+
+ private void logd(String message) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, message);
+ }
+ }
+
+ /**
+ * Listener used to notify when the dialog is hidden
+ */
+ interface OnHideListener {
+ void onHide();
+ }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
index 0a167d9acf982..7cd6adbb3952b 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
@@ -18,29 +18,60 @@ package com.android.systemui.statusbar.car;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.car.trust.CarTrustAgentEnrollmentManager;
+import android.car.userlib.CarUserManagerHelper;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.UserInfo;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
import android.view.View;
import android.view.ViewStub;
import androidx.recyclerview.widget.GridLayoutManager;
import com.android.systemui.R;
+import com.android.systemui.statusbar.car.UserGridRecyclerView.UserRecord;
/**
* Manages the fullscreen user switcher.
*/
public class FullscreenUserSwitcher {
+ private static final String TAG = FullscreenUserSwitcher.class.getSimpleName();
+ // Because user 0 is headless, user count for single user is 2
+ private static final int NUMBER_OF_BACKGROUND_USERS = 1;
private final UserGridRecyclerView mUserGridView;
private final View mParent;
private final int mShortAnimDuration;
private final CarStatusBar mStatusBar;
+ private final Context mContext;
+ private final UserManager mUserManager;
+ private final CarTrustAgentEnrollmentManager mEnrollmentManager;
+ private CarTrustAgentUnlockDialogHelper mUnlockDialogHelper;
+ private UserGridRecyclerView.UserRecord mSelectedUser;
+ private CarUserManagerHelper mCarUserManagerHelper;
+ private final BroadcastReceiver mUserUnlockReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "user 0 is unlocked, SharedPreference is accessible.");
+ }
+ showDialogForInitialUser();
+ mContext.unregisterReceiver(mUserUnlockReceiver);
+ }
+ };
- public FullscreenUserSwitcher(CarStatusBar statusBar, ViewStub containerStub, Context context) {
+
+ public FullscreenUserSwitcher(CarStatusBar statusBar, ViewStub containerStub,
+ CarTrustAgentEnrollmentManager enrollmentManager, Context context) {
mStatusBar = statusBar;
mParent = containerStub.inflate();
- // Hide the user grid by default. It will only be made visible by clicking on a cancel
- // button in a bouncer.
- hide();
+ mEnrollmentManager = enrollmentManager;
+ mContext = context;
+
View container = mParent.findViewById(R.id.container);
// Initialize user grid.
@@ -50,9 +81,51 @@ public class FullscreenUserSwitcher {
mUserGridView.setLayoutManager(layoutManager);
mUserGridView.buildAdapter();
mUserGridView.setUserSelectionListener(this::onUserSelected);
+ mCarUserManagerHelper = new CarUserManagerHelper(context);
+ mUnlockDialogHelper = new CarTrustAgentUnlockDialogHelper(mContext);
+ mUserManager = mContext.getSystemService(UserManager.class);
mShortAnimDuration = container.getResources()
.getInteger(android.R.integer.config_shortAnimTime);
+ IntentFilter filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
+ if (mUserManager.isUserUnlocked(UserHandle.USER_SYSTEM)) {
+ // User0 is unlocked, switched to the initial user
+ showDialogForInitialUser();
+ } else {
+ // listen to USER_UNLOCKED
+ mContext.registerReceiverAsUser(mUserUnlockReceiver,
+ UserHandle.getUserHandleForUid(UserHandle.USER_SYSTEM),
+ filter,
+ /* broadcastPermission= */ null,
+ /* scheduler */ null);
+ }
+ }
+
+ private void showDialogForInitialUser() {
+ int initialUser = mCarUserManagerHelper.getInitialUser();
+ UserInfo initialUserInfo = mUserManager.getUserInfo(initialUser);
+ mSelectedUser = new UserRecord(initialUserInfo,
+ /* isStartGuestSession= */ false,
+ /* isAddUser= */ false,
+ /* isForeground= */ true);
+ // For single user without trusted device, hide the user switcher.
+ if (!hasMultipleUsers() && !hasTrustedDevice(initialUser)) {
+ dismissUserSwitcher();
+ return;
+ }
+ // Show unlock dialog for initial user
+ if (hasTrustedDevice(initialUser)) {
+ mUnlockDialogHelper.showUnlockDialogAfterDelay(initialUser,
+ () -> dismissUserSwitcher());
+ }
+ }
+
+ /**
+ * Check if there is only one possible user to login in.
+ * In a Multi-User system there is always one background user (user 0)
+ */
+ private boolean hasMultipleUsers() {
+ return mUserManager.getUserCount() > NUMBER_OF_BACKGROUND_USERS + 1;
}
/**
@@ -77,14 +150,33 @@ public class FullscreenUserSwitcher {
}
/**
- * Every time user clicks on an item in the switcher, we hide the switcher, either
- * gradually or immediately.
+ * Every time user clicks on an item in the switcher, if the clicked user has no trusted device,
+ * we hide the switcher, either gradually or immediately.
*
- * We dismiss the entire keyguard if user clicked on the foreground user (user we're already
- * logged in as).
+ * If the user has trusted device, we show an unlock dialog to notify user the unlock state.
+ * When the unlock dialog is dismissed by user, we hide the unlock dialog and the switcher.
+ *
+ * We dismiss the entire keyguard when we hide the switcher if user clicked on the foreground
+ * user (user we're already logged in as).
*/
private void onUserSelected(UserGridRecyclerView.UserRecord record) {
- if (record.mIsForeground) {
+ mSelectedUser = record;
+ if (hasTrustedDevice(record.mInfo.id)) {
+ mUnlockDialogHelper.showUnlockDialog(record.mInfo.id, () -> dismissUserSwitcher());
+ return;
+ }
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "no trusted device enrolled for uid: " + record.mInfo.id);
+ }
+ dismissUserSwitcher();
+ }
+
+ private void dismissUserSwitcher() {
+ if (mSelectedUser == null) {
+ Log.e(TAG, "Request to dismiss user switcher, but no user selected");
+ return;
+ }
+ if (mSelectedUser.mIsForeground) {
hide();
mStatusBar.dismissKeyguard();
return;
@@ -106,4 +198,8 @@ public class FullscreenUserSwitcher {
});
}
+
+ private boolean hasTrustedDevice(int uid) {
+ return !mEnrollmentManager.getEnrolledDeviceInfoForUser(uid).isEmpty();
+ }
}