Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 64d64fdd authored by Vania Januar's avatar Vania Januar
Browse files

Disambiguate default notes app user from stylus settings

Users can now select between work or personal notes app to be the app
that opens when the stylus tail button is pressed via a dialog.

Bug: 278555728
Test: StylusDevicesControllerTest
Change-Id: I9c63de6f11deb357b0497c7b972d4ac19b876e1f
parent f6b49a02
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -253,8 +253,10 @@
    <!-- Title for stylus device details page [CHAR LIMIT=50] -->
    <string name="stylus_device_details_title">Stylus</string>
    <!-- Preference title for setting the default note taking app [CHAR LIMIT=none] -->
    <string name="stylus_default_notes_app">Default notes app</string>
    <!-- Preference title for setting the app that opens when stylus button is pressed [CHAR LIMIT=none] -->
    <string name="stylus_default_notes_app">Tail button press</string>
    <!-- Summary for the app that opens when tail button is pressed, if set to a work profile app [CHAR LIMIT=none] -->
    <string name="stylus_default_notes_summary_work"><xliff:g id="app_name" example="WhatsApp">%s</xliff:g> (Work profile)</string>
    <!-- Preference title for toggling whether handwriting in textfields is enabled [CHAR LIMIT=none] -->
    <string name="stylus_textfield_handwriting">Write in text fields</string>
    <!-- Preference title for toggling whether stylus button presses are ignored [CHAR LIMIT=none] -->
+76 −4
Original line number Diff line number Diff line
@@ -16,12 +16,17 @@

package com.android.settings.connecteddevice.stylus;

import android.app.Dialog;
import android.app.role.RoleManager;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.text.TextUtils;
@@ -38,6 +43,8 @@ import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;

import com.android.settings.R;
import com.android.settings.dashboard.profileselector.ProfileSelectDialog;
import com.android.settings.dashboard.profileselector.UserAdapter;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.core.AbstractPreferenceController;
@@ -45,6 +52,7 @@ import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnResume;

import java.util.ArrayList;
import java.util.List;

/**
@@ -73,6 +81,9 @@ public class StylusDevicesController extends AbstractPreferenceController implem
    @VisibleForTesting
    PreferenceCategory mPreferencesContainer;

    @VisibleForTesting
    Dialog mDialog;

    public StylusDevicesController(Context context, InputDevice inputDevice,
            CachedBluetoothDevice cachedBluetoothDevice, Lifecycle lifecycle) {
        super(context);
@@ -100,8 +111,8 @@ public class StylusDevicesController extends AbstractPreferenceController implem
        pref.setOnPreferenceClickListener(this);
        pref.setEnabled(true);

        List<String> roleHolders = rm.getRoleHoldersAsUser(RoleManager.ROLE_NOTES,
                mContext.getUser());
        UserHandle user = getDefaultNoteTaskProfile();
        List<String> roleHolders = rm.getRoleHoldersAsUser(RoleManager.ROLE_NOTES, user);
        if (roleHolders.isEmpty()) {
            pref.setSummary(R.string.default_app_none);
            return pref;
@@ -117,7 +128,13 @@ public class StylusDevicesController extends AbstractPreferenceController implem
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "Notes role package not found.");
        }

        if (mContext.getSystemService(UserManager.class).isManagedProfile(user.getIdentifier())) {
            pref.setSummary(
                    mContext.getString(R.string.stylus_default_notes_summary_work, appName));
        } else {
            pref.setSummary(appName);
        }
        return pref;
    }

@@ -155,7 +172,13 @@ public class StylusDevicesController extends AbstractPreferenceController implem
                String packageName = pm.getPermissionControllerPackageName();
                Intent intent = new Intent(Intent.ACTION_MANAGE_DEFAULT_APP).setPackage(
                        packageName).putExtra(Intent.EXTRA_ROLE_NAME, RoleManager.ROLE_NOTES);

                List<UserHandle> users = getUserAndManagedProfiles();
                if (users.size() <= 1) {
                    mContext.startActivity(intent);
                } else {
                    createAndShowProfileSelectDialog(intent, users);
                }
                break;
            case KEY_HANDWRITING:
                Settings.Secure.putInt(mContext.getContentResolver(),
@@ -229,6 +252,55 @@ public class StylusDevicesController extends AbstractPreferenceController implem
        return inputMethod != null && inputMethod.supportsStylusHandwriting();
    }

    private List<UserHandle> getUserAndManagedProfiles() {
        UserManager um = mContext.getSystemService(UserManager.class);
        final ArrayList<UserHandle> userManagedProfiles = new ArrayList<>();
        // Add the current user, then add all the associated managed profiles.
        final UserHandle currentUser = Process.myUserHandle();
        userManagedProfiles.add(currentUser);

        final List<UserInfo> userInfos = um.getUsers();
        for (UserInfo info : userInfos) {
            if (um.isManagedProfile(info.id)
                    && um.getProfileParent(info.id).id == currentUser.getIdentifier()) {
                userManagedProfiles.add(UserHandle.of(info.id));
            }
        }
        return userManagedProfiles;
    }

    private UserHandle getDefaultNoteTaskProfile() {
        final int userId = Secure.getInt(
                mContext.getContentResolver(),
                Secure.DEFAULT_NOTE_TASK_PROFILE,
                UserHandle.myUserId());
        return UserHandle.of(userId);
    }

    @VisibleForTesting
    UserAdapter.OnClickListener createProfileDialogClickCallback(
            Intent intent, List<UserHandle> users) {
        // TODO(b/281659827): improve UX flow for when activity is cancelled
        return (int position) -> {
            intent.putExtra(Intent.EXTRA_USER, users.get(position));

            Secure.putInt(mContext.getContentResolver(),
                    Secure.DEFAULT_NOTE_TASK_PROFILE,
                    users.get(position).getIdentifier());
            mContext.startActivity(intent);

            mDialog.dismiss();
        };
    }

    private void createAndShowProfileSelectDialog(Intent intent, List<UserHandle> users) {
        mDialog = ProfileSelectDialog.createDialog(
                mContext,
                users,
                createProfileDialogClickCallback(intent, users));
        mDialog.show();
    }

    /**
     * Identifies whether a device is a stylus using the associated {@link InputDevice} or
     * {@link CachedBluetoothDevice}.
+74 −5
Original line number Diff line number Diff line
@@ -18,6 +18,10 @@ package com.android.settings.connecteddevice.stylus;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doNothing;
@@ -27,13 +31,17 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Dialog;
import android.app.role.RoleManager;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.view.InputDevice;
@@ -48,6 +56,7 @@ import androidx.preference.SwitchPreference;
import androidx.test.core.app.ApplicationProvider;

import com.android.settings.R;
import com.android.settings.dashboard.profileselector.UserAdapter;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.core.lifecycle.Lifecycle;

@@ -59,7 +68,9 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

@RunWith(RobolectricTestRunner.class)
public class StylusDevicesControllerTest {
@@ -79,6 +90,8 @@ public class StylusDevicesControllerTest {
    @Mock
    private PackageManager mPm;
    @Mock
    private UserManager mUserManager;
    @Mock
    private RoleManager mRm;
    @Mock
    private Lifecycle mLifecycle;
@@ -87,7 +100,6 @@ public class StylusDevicesControllerTest {
    @Mock
    private BluetoothDevice mBluetoothDevice;


    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
@@ -101,6 +113,7 @@ public class StylusDevicesControllerTest {

        when(mContext.getSystemService(InputMethodManager.class)).thenReturn(mImm);
        when(mContext.getSystemService(RoleManager.class)).thenReturn(mRm);
        when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
        doNothing().when(mContext).startActivity(any());

        when(mImm.getCurrentInputMethodInfo()).thenReturn(mInputMethodInfo);
@@ -115,6 +128,8 @@ public class StylusDevicesControllerTest {
        when(mPm.getApplicationInfo(eq(NOTES_PACKAGE_NAME),
                any(PackageManager.ApplicationInfoFlags.class))).thenReturn(new ApplicationInfo());
        when(mPm.getApplicationLabel(any(ApplicationInfo.class))).thenReturn(NOTES_APP_LABEL);
        when(mUserManager.getUsers()).thenReturn(Arrays.asList(new UserInfo(0, "default", 0)));
        when(mUserManager.isManagedProfile(anyInt())).thenReturn(false);

        when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);

@@ -228,21 +243,35 @@ public class StylusDevicesControllerTest {
        when(mInputMethodInfo.supportsStylusHandwriting()).thenReturn(false);

        showScreen(mController);
        Preference handwritingPref = mPreferenceContainer.getPreference(1);

        Preference handwritingPref = mPreferenceContainer.getPreference(1);
        assertThat(handwritingPref.isVisible()).isFalse();
    }

    @Test
    public void defaultNotesPreference_showsNotesRoleApp() {
    public void defaultNotesPreference_singleUser_showsNotesRoleApp() {
        showScreen(mController);
        Preference defaultNotesPref = mPreferenceContainer.getPreference(0);

        Preference defaultNotesPref = mPreferenceContainer.getPreference(0);
        assertThat(defaultNotesPref.getTitle().toString()).isEqualTo(
                mContext.getString(R.string.stylus_default_notes_app));
        assertThat(defaultNotesPref.getSummary().toString()).isEqualTo(NOTES_APP_LABEL.toString());
    }

    @Test
    public void defaultNotesPreference_workProfileUser_showsWorkNotesRoleApp() {
        when(mUserManager.isManagedProfile(0)).thenReturn(true);

        showScreen(mController);

        Preference defaultNotesPref = mPreferenceContainer.getPreference(0);
        assertThat(defaultNotesPref.getTitle().toString()).isEqualTo(
                mContext.getString(R.string.stylus_default_notes_app));
        assertThat(defaultNotesPref.getSummary().toString()).isEqualTo(
                mContext.getString(R.string.stylus_default_notes_summary_work,
                        NOTES_APP_LABEL.toString()));
    }

    @Test
    public void defaultNotesPreference_roleHolderChanges_updatesPreference() {
        showScreen(mController);
@@ -267,7 +296,7 @@ public class StylusDevicesControllerTest {
    }

    @Test
    public void defaultNotesPreferenceClick_sendsManageDefaultRoleIntent() {
    public void defaultNotesPreferenceClick_singleUser_sendsManageDefaultRoleIntent() {
        final String permissionPackageName = "permissions.package";
        when(mPm.getPermissionControllerPackageName()).thenReturn(permissionPackageName);
        final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
@@ -282,6 +311,46 @@ public class StylusDevicesControllerTest {
        assertThat(intent.getPackage()).isEqualTo(permissionPackageName);
        assertThat(intent.getStringExtra(Intent.EXTRA_ROLE_NAME)).isEqualTo(
                RoleManager.ROLE_NOTES);
        assertNull(mController.mDialog);
    }

    @Test
    public void defaultNotesPreferenceClick_multiUser_showsProfileSelectorDialog() {
        mContext.setTheme(R.style.Theme_AppCompat);
        final String permissionPackageName = "permissions.package";
        final UserHandle currentUser = Process.myUserHandle();
        List<UserInfo> userInfos = Arrays.asList(
                new UserInfo(currentUser.getIdentifier(), "current", 0),
                new UserInfo(1, "profile", UserInfo.FLAG_PROFILE)
        );
        when(mUserManager.getUsers()).thenReturn(userInfos);
        when(mUserManager.isManagedProfile(1)).thenReturn(true);
        when(mUserManager.getUserInfo(currentUser.getIdentifier())).thenReturn(userInfos.get(0));
        when(mUserManager.getUserInfo(1)).thenReturn(userInfos.get(1));
        when(mUserManager.getProfileParent(1)).thenReturn(userInfos.get(0));
        when(mPm.getPermissionControllerPackageName()).thenReturn(permissionPackageName);

        showScreen(mController);
        Preference defaultNotesPref = mPreferenceContainer.getPreference(0);
        mController.onPreferenceClick(defaultNotesPref);

        assertTrue(mController.mDialog.isShowing());
    }

    @Test
    public void profileSelectDialogClickCallback_onClick_sendsIntent() {
        Intent intent = new Intent();
        UserHandle user1 = mock(UserHandle.class);
        UserHandle user2 = mock(UserHandle.class);
        List<UserHandle> users = Arrays.asList(new UserHandle[] {user1, user2});
        mController.mDialog = mock(Dialog.class);
        UserAdapter.OnClickListener callback = mController
                .createProfileDialogClickCallback(intent, users);

        callback.onClick(1);

        assertEquals(intent.getExtra(Intent.EXTRA_USER), user2);
        verify(mContext).startActivity(intent);
    }

    @Test