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)