Merge "Put "Add user" dialog within activity to capture focus" into main
diff --git a/packages/SettingsLib/AndroidManifest.xml b/packages/SettingsLib/AndroidManifest.xml
index 412ff29..473b7eb 100644
--- a/packages/SettingsLib/AndroidManifest.xml
+++ b/packages/SettingsLib/AndroidManifest.xml
@@ -21,6 +21,13 @@
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<application>
+ <activity
+ android:name=".users.CreateUserActivity"
+ android:excludeFromRecents="true"
+ android:exported="false"
+ android:finishOnCloseSystemDialogs="true"
+ android:launchMode="singleInstance"
+ android:theme="@style/Theme.Transparent"/>
</application>
</manifest>
diff --git a/packages/SettingsLib/res/layout/activity_create_new_user.xml b/packages/SettingsLib/res/layout/activity_create_new_user.xml
new file mode 100644
index 0000000..7453b53
--- /dev/null
+++ b/packages/SettingsLib/res/layout/activity_create_new_user.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 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.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent"
+ android:orientation="vertical">
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/values/styles.xml b/packages/SettingsLib/res/values/styles.xml
index 3326b60..7ab096f 100644
--- a/packages/SettingsLib/res/values/styles.xml
+++ b/packages/SettingsLib/res/values/styles.xml
@@ -116,4 +116,13 @@
<item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
<item name="android:textSize">16dp</item>
</style>
+
+ <style name="Theme.Transparent" parent="@android:style/Theme.DeviceDefault.Settings">
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowContentOverlay">@null</item>
+ </style>
+
</resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java
new file mode 100644
index 0000000..c5e6f60
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserActivity.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2025 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.settingslib.users;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.MotionEvent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settingslib.R;
+
+
+public class CreateUserActivity extends Activity {
+ private static final String TAG = "CreateUserActivity";
+
+ public static final String EXTRA_USER_NAME = "new_user_name";
+ public static final String EXTRA_IS_ADMIN = "is_admin";
+ public static final String EXTRA_USER_ICON_PATH = "user_icon_path";
+ private static final String DIALOG_STATE_KEY = "create_user_dialog_state";
+ private static final String EXTRA_CAN_CREATE_ADMIN = "can_create_admin";
+ private static final String EXTRA_FILE_AUTHORITY = "file_authority";
+
+ private CreateUserDialogController mCreateUserDialogController;
+ @VisibleForTesting
+ Dialog mSetupUserDialog;
+
+
+ /**
+ * Creates intent to start CreateUserActivity
+ */
+ public static @NonNull Intent createIntentForStart(@NonNull Context context,
+ boolean canCreateAdminUser, @NonNull String fileAuth) {
+ Intent intent = new Intent(context, CreateUserActivity.class);
+ intent.putExtra(EXTRA_CAN_CREATE_ADMIN, canCreateAdminUser);
+ intent.putExtra(EXTRA_FILE_AUTHORITY, fileAuth);
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+
+ mCreateUserDialogController = new CreateUserDialogController(
+ intent.getStringExtra(EXTRA_FILE_AUTHORITY));
+ setContentView(R.layout.activity_create_new_user);
+ if (savedInstanceState != null) {
+ mCreateUserDialogController.onRestoreInstanceState(savedInstanceState);
+ }
+ mSetupUserDialog = createDialog(intent.getBooleanExtra(EXTRA_CAN_CREATE_ADMIN, false));
+ mSetupUserDialog.show();
+ }
+
+ @Override
+ protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ Bundle savedDialogState = savedInstanceState.getBundle(DIALOG_STATE_KEY);
+ if (savedDialogState != null && mSetupUserDialog != null) {
+ mSetupUserDialog.onRestoreInstanceState(savedDialogState);
+ }
+ }
+
+ private Dialog createDialog(boolean canCreateAdminUser) {
+ return mCreateUserDialogController.createDialog(
+ this,
+ this::startActivity,
+ canCreateAdminUser,
+ this::setSuccessResult,
+ this::cancel
+ );
+ }
+
+ @Override
+ public boolean onTouchEvent(@Nullable MotionEvent event) {
+ onBackInvoked();
+ return super.onTouchEvent(event);
+ }
+
+ private void onBackInvoked() {
+ if (mSetupUserDialog != null) {
+ mSetupUserDialog.dismiss();
+ }
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ @VisibleForTesting
+ void setSuccessResult(String userName, Drawable userIcon, String path, Boolean isAdmin) {
+ Intent intent = new Intent(this, CreateUserActivity.class);
+ intent.putExtra(EXTRA_USER_NAME, userName);
+ intent.putExtra(EXTRA_IS_ADMIN, isAdmin);
+ intent.putExtra(EXTRA_USER_ICON_PATH, path);
+
+ mSetupUserDialog.dismiss();
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+
+ @VisibleForTesting
+ void cancel() {
+ mSetupUserDialog.dismiss();
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ @Override
+ protected void onSaveInstanceState(@NonNull Bundle outState) {
+ if (mSetupUserDialog != null && mSetupUserDialog.isShowing()) {
+ outState.putBundle(DIALOG_STATE_KEY, mSetupUserDialog.onSaveInstanceState());
+ }
+ mCreateUserDialogController.onSaveInstanceState(outState);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ mCreateUserDialogController.onActivityResult(requestCode, resultCode, data);
+ }
+
+ private void startActivity(Intent intent, int requestCode) {
+ startActivityForResult(intent, requestCode);
+ mCreateUserDialogController.startingActivityForResult();
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
index d71b337..d9f1b63 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
@@ -242,7 +242,7 @@
.setMessage(messageResId)
.setNegativeButtonText(R.string.cancel)
.setPositiveButtonText(R.string.next);
- mCustomDialogHelper.requestFocusOnTitle();
+ focus();
break;
case GRANT_ADMIN_DIALOG:
mEditUserInfoView.setVisibility(View.GONE);
@@ -255,7 +255,7 @@
.setMessage(R.string.user_grant_admin_message)
.setNegativeButtonText(R.string.back)
.setPositiveButtonText(R.string.next);
- mCustomDialogHelper.requestFocusOnTitle();
+ focus();
if (mIsAdmin == null) {
mCustomDialogHelper.setButtonEnabled(false);
}
@@ -267,7 +267,7 @@
.setTitle(R.string.user_info_settings_title)
.setNegativeButtonText(R.string.back)
.setPositiveButtonText(R.string.done);
- mCustomDialogHelper.requestFocusOnTitle();
+ focus();
mEditUserInfoView.setVisibility(View.VISIBLE);
mGrantAdminView.setVisibility(View.GONE);
break;
@@ -282,7 +282,7 @@
mCustomDialogHelper.getDialog().dismiss();
break;
case EXIT_DIALOG:
- mCustomDialogHelper.getDialog().dismiss();
+ finish();
break;
default:
if (mCurrentState < EXIT_DIALOG) {
@@ -394,13 +394,21 @@
return mCustomDialogHelper != null && mCustomDialogHelper.getDialog() != null;
}
+ void focus() {
+ mCustomDialogHelper.requestFocusOnTitle();
+ }
+
/**
* Runs callback and clears saved values after dialog is dismissed.
*/
public void finish() {
if (mCurrentState == CREATE_USER_AND_CLOSE) {
if (mSuccessCallback != null) {
- mSuccessCallback.onSuccess(mUserName, mNewUserIcon, Boolean.TRUE.equals(mIsAdmin));
+ if (mEditUserPhotoController != null && mCachedDrawablePath == null) {
+ mCachedDrawablePath = mEditUserPhotoController.getCachedDrawablePath();
+ }
+ mSuccessCallback.onSuccess(mUserName, mNewUserIcon, mCachedDrawablePath,
+ Boolean.TRUE.equals(mIsAdmin));
}
} else {
if (mCancelCallback != null) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java b/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java
index 3d18b59..eed608e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/NewUserData.java
@@ -18,6 +18,8 @@
import android.graphics.drawable.Drawable;
+import androidx.annotation.Nullable;
+
/**
* Defines a callback when a new user data is filled out.
*/
@@ -27,8 +29,10 @@
* Consumes data relevant to new user that needs to be created.
* @param userName New user name.
* @param userImage New user icon.
+ * @param iconPath New user icon path.
* @param isNewUserAdmin A boolean that indicated whether new user has admin status.
*/
- void onSuccess(String userName, Drawable userImage, Boolean isNewUserAdmin);
+ void onSuccess(@Nullable String userName, @Nullable Drawable userImage,
+ @Nullable String iconPath, @Nullable Boolean isNewUserAdmin);
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java
new file mode 100644
index 0000000..f58eb7c
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserActivityTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2025 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.settingslib.users;
+
+import static com.android.settingslib.users.CreateUserActivity.EXTRA_IS_ADMIN;
+import static com.android.settingslib.users.CreateUserActivity.EXTRA_USER_ICON_PATH;
+import static com.android.settingslib.users.CreateUserActivity.EXTRA_USER_NAME;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.MotionEvent;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class CreateUserActivityTest {
+
+ private static final String TEST_USER_NAME = "test_user";
+ private static final String TEST_USER_ICON_PATH = "/test_path";
+ private static final boolean TEST_IS_ADMIN = true;
+
+ private Context mContext;
+ private CreateUserActivity mCreateUserActivity;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mCreateUserActivity = Robolectric.buildActivity(CreateUserActivity.class).setup().get();
+ }
+
+ @Test
+ public void startActivity_startsActivityForResult() {
+ Intent activityIntent = CreateUserActivity.createIntentForStart(mContext, true, "");
+ mCreateUserActivity.startActivity(activityIntent, null);
+
+ assertThat(shadowOf(mCreateUserActivity).getNextStartedActivityForResult().intent)
+ .isEqualTo(activityIntent);
+ }
+
+ @Test
+ public void onTouchEvent_dismissesDialogAndCancelsResult() {
+ mCreateUserActivity.onTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0, 0,
+ 0));
+
+ assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse();
+ assertThat(shadowOf(mCreateUserActivity).getResultCode())
+ .isEqualTo(Activity.RESULT_CANCELED);
+ }
+
+ @Test
+ public void setSuccessResult_dismissesDialogAndSetsSuccessResult() {
+ Drawable mockDrawable = mock(Drawable.class);
+
+ mCreateUserActivity.setSuccessResult(TEST_USER_NAME, mockDrawable, TEST_USER_ICON_PATH,
+ TEST_IS_ADMIN);
+
+ assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse();
+ assertThat(shadowOf(mCreateUserActivity).getResultCode()).isEqualTo(Activity.RESULT_OK);
+
+ Intent resultIntent = shadowOf(mCreateUserActivity).getResultIntent();
+ assertThat(resultIntent.getStringExtra(EXTRA_USER_NAME)).isEqualTo(TEST_USER_NAME);
+ assertThat(resultIntent.getBooleanExtra(EXTRA_IS_ADMIN, false)).isEqualTo(TEST_IS_ADMIN);
+ assertThat(resultIntent.getStringExtra(EXTRA_USER_ICON_PATH))
+ .isEqualTo(TEST_USER_ICON_PATH);
+ }
+
+ @Test
+ public void cancel_dismissesDialogAndSetsCancelResult() {
+ mCreateUserActivity.cancel();
+
+ assertThat(mCreateUserActivity.mSetupUserDialog.isShowing()).isFalse();
+ assertThat(shadowOf(mCreateUserActivity).getResultCode())
+ .isEqualTo(Activity.RESULT_CANCELED);
+ }
+
+ @Test
+ public void onSaveInstanceState_savesDialogState() {
+ Bundle outState = new Bundle();
+ mCreateUserActivity.onSaveInstanceState(outState);
+
+ CreateUserActivity restoredActivity =
+ Robolectric.buildActivity(CreateUserActivity.class).setup(outState).get();
+
+ assertThat(restoredActivity.mSetupUserDialog).isNotNull();
+ assertThat(restoredActivity.mSetupUserDialog.isShowing()).isTrue();
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java
index 6831222..e602323 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/users/CreateUserDialogControllerTest.java
@@ -211,7 +211,7 @@
editText.setText(expectedNewName);
next.performClick();
verify(successCallback, times(1))
- .onSuccess(expectedNewName, null, true);
+ .onSuccess(expectedNewName, null, null, true);
verifyNoInteractions(cancelCallback);
}
@@ -233,7 +233,7 @@
editText.setText(expectedNewName);
next.performClick();
verify(successCallback, times(1))
- .onSuccess(expectedNewName, null, false);
+ .onSuccess(expectedNewName, null, null, false);
verifyNoInteractions(cancelCallback);
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
index 32f2ca6..367f54c 100644
--- a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
@@ -147,7 +147,7 @@
super.onDestroy();
}
- private void addUserNow(String userName, Drawable userIcon, Boolean isAdmin) {
+ private void addUserNow(String userName, Drawable userIcon, String iconPath, Boolean isAdmin) {
mSetupUserDialog.dismiss();
userName = (userName == null || userName.trim().isEmpty())
? getString(com.android.settingslib.R.string.user_new_user_name)