Merge "Add a stylus controller to Bluetooth Device Details."

This commit is contained in:
Vania Januar
2022-12-01 10:16:31 +00:00
committed by Android (Google) Code Review
9 changed files with 687 additions and 19 deletions

View File

@@ -0,0 +1,25 @@
<!--
Copyright (C) 2022 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M7,17H14V15H7ZM7,13H17V11H7ZM7,9H17V7H7ZM5,21Q4.175,21 3.587,20.413Q3,19.825 3,19V5Q3,4.175 3.587,3.587Q4.175,3 5,3H19Q19.825,3 20.413,3.587Q21,4.175 21,5V19Q21,19.825 20.413,20.413Q19.825,21 19,21ZM5,19H19Q19,19 19,19Q19,19 19,19V5Q19,5 19,5Q19,5 19,5H5Q5,5 5,5Q5,5 5,5V19Q5,19 5,19Q5,19 5,19ZM5,5Q5,5 5,5Q5,5 5,5V19Q5,19 5,19Q5,19 5,19Q5,19 5,19Q5,19 5,19V5Q5,5 5,5Q5,5 5,5Z"/>
</vector>

25
res/drawable/ic_block.xml Normal file
View File

@@ -0,0 +1,25 @@
<!--
Copyright (C) 2022 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M12,22Q9.925,22 8.1,21.212Q6.275,20.425 4.925,19.075Q3.575,17.725 2.788,15.9Q2,14.075 2,12Q2,9.925 2.788,8.1Q3.575,6.275 4.925,4.925Q6.275,3.575 8.1,2.787Q9.925,2 12,2Q14.075,2 15.9,2.787Q17.725,3.575 19.075,4.925Q20.425,6.275 21.212,8.1Q22,9.925 22,12Q22,14.075 21.212,15.9Q20.425,17.725 19.075,19.075Q17.725,20.425 15.9,21.212Q14.075,22 12,22ZM12,20Q15.35,20 17.675,17.675Q20,15.35 20,12Q20,10.65 19.562,9.4Q19.125,8.15 18.3,7.1L7.1,18.3Q8.15,19.125 9.4,19.562Q10.65,20 12,20ZM5.7,16.9 L16.9,5.7Q15.85,4.875 14.6,4.438Q13.35,4 12,4Q8.65,4 6.325,6.325Q4,8.65 4,12Q4,13.35 4.438,14.6Q4.875,15.85 5.7,16.9Z"/>
</vector>

View File

@@ -0,0 +1,25 @@
<!--
Copyright (C) 2022 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M2,21V19H22V21ZM19,17V3H20.5V17ZM4,17 L9.25,3H11.75L17,17H14.6L13.35,13.4H7.7L6.4,17ZM8.4,11.4H12.6L10.55,5.6H10.45Z"/>
</vector>

View File

@@ -228,6 +228,17 @@
<!-- Title to see all the previous connected devices [CHAR LIMIT=50] -->
<string name="previous_connected_see_all">See all</string>
<!-- 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">System note taking app</string>
<!-- Preference title for toggling whether handwriting in textfields is enabled [CHAR LIMIT=none] -->
<string name="stylus_textfield_handwriting">Stylus writing in textfields</string>
<!-- Preference title for toggling whether stylus button presses are ignored [CHAR LIMIT=none] -->
<string name="stylus_ignore_button">Ignore all stylus button presses</string>
<!-- Name shown in a USI stylus header in device details page [CHAR LIMIT=60] -->
<string name="stylus_usi_header_title">USI stylus</string>
<!-- Date & time settings screen title -->
<string name="date_and_time">Date &amp; time</string>

View File

@@ -44,15 +44,18 @@
<com.android.settingslib.widget.ButtonPreference
android:key="hearing_aid_pair_other_button"
android:gravity="center" />
android:gravity="center"/>
<com.android.settings.applications.SpacePreference
android:key="hearing_aid_space_layout"
android:layout_height="8dp" />
android:layout_height="8dp"/>
<com.android.settingslib.widget.ActionButtonsPreference
android:key="action_buttons"
settings:allowDividerBelow="true"/>
<PreferenceCategory
android:key="device_stylus"/>
<com.android.settings.slices.SlicePreference
android:key="bt_extra_control"
settings:controller="com.android.settings.slices.SlicePreferenceController"

View File

@@ -23,12 +23,15 @@ import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.res.TypedArray;
import android.hardware.input.InputManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserManager;
import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.FeatureFlagUtils;
import android.util.Log;
import android.view.InputDevice;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -37,9 +40,11 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.connecteddevice.stylus.StylusDevicesController;
import com.android.settings.core.SettingsUIDeviceConfig;
import com.android.settings.dashboard.RestrictedDashboardFragment;
import com.android.settings.overlay.FeatureFactory;
@@ -72,6 +77,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
CachedBluetoothDevice getDevice(String deviceAddress);
LocalBluetoothManager getManager(Context context);
UserManager getUserManager();
}
@@ -85,6 +91,9 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
@VisibleForTesting
CachedBluetoothDevice mCachedDevice;
@Nullable
InputDevice mInputDevice;
private UserManager mUserManager;
public BluetoothDeviceDetailsFragment() {
@@ -118,6 +127,21 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
return getSystemService(UserManager.class);
}
@Nullable
@VisibleForTesting
InputDevice getInputDevice(Context context) {
InputManager im = context.getSystemService(InputManager.class);
for (int deviceId : im.getInputDeviceIds()) {
String btAddress = im.getInputDeviceBluetoothAddress(deviceId);
if (btAddress != null && btAddress.equals(mDeviceAddress)) {
return im.getInputDevice(deviceId);
}
}
return null;
}
public static BluetoothDeviceDetailsFragment newInstance(String deviceAddress) {
Bundle args = new Bundle(1);
args.putString(KEY_DEVICE_ADDRESS, deviceAddress);
@@ -132,6 +156,12 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
mManager = getLocalBluetoothManager(context);
mCachedDevice = getCachedDevice(mDeviceAddress);
mUserManager = getUserManager();
if (FeatureFlagUtils.isEnabled(context,
FeatureFlagUtils.SETTINGS_SHOW_STYLUS_PREFERENCES)) {
mInputDevice = getInputDevice(context);
}
super.onAttach(context);
if (mCachedDevice == null) {
// Close this page if device is null with invalid device mac address
@@ -191,6 +221,12 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitleForInputDevice();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -269,6 +305,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
mCachedDevice, lifecycle));
controllers.add(new BluetoothDetailsMacAddressController(context, this, mCachedDevice,
lifecycle));
controllers.add(new StylusDevicesController(context, mInputDevice, lifecycle));
controllers.add(new BluetoothDetailsRelatedToolsController(context, this, mCachedDevice,
lifecycle));
controllers.add(new BluetoothDetailsPairOtherController(context, this, mCachedDevice,
@@ -289,4 +326,16 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
resolvedAttributes.recycle();
return width;
}
@VisibleForTesting
void setTitleForInputDevice() {
// TODO(b/254835745) once source filter for bt stylus merged
// && mInputDevice.supportsSource(InputDevice.SOURCE_BLUETOOTH_STYLUS))
if (mInputDevice != null) {
// This will override the default R.string.device_details_title "Device Details"
// that will show on non-stylus bluetooth devices.
// That title is set via the manifest and also from BluetoothDeviceUpdater.
getActivity().setTitle(getContext().getString(R.string.stylus_device_details_title));
}
}
}

View File

@@ -0,0 +1,199 @@
/*
* Copyright (C) 2022 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.settings.connecteddevice.stylus;
import android.app.role.RoleManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.provider.Settings;
import android.util.Log;
import android.view.InputDevice;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;
import com.android.settings.R;
import com.android.settingslib.core.AbstractPreferenceController;
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.List;
/**
* This class adds stylus preferences.
*/
public class StylusDevicesController extends AbstractPreferenceController implements
Preference.OnPreferenceClickListener, LifecycleObserver, OnResume {
@VisibleForTesting
static final String KEY_STYLUS = "device_stylus";
@VisibleForTesting
static final String KEY_HANDWRITING = "handwriting_switch";
@VisibleForTesting
static final String KEY_IGNORE_BUTTON = "ignore_button";
@VisibleForTesting
static final String KEY_DEFAULT_NOTES = "default_notes";
private static final String TAG = "StylusDevicesController";
@Nullable
private final InputDevice mInputDevice;
@VisibleForTesting
PreferenceCategory mPreferencesContainer;
public StylusDevicesController(Context context, InputDevice inputDevice, Lifecycle lifecycle) {
super(context);
mInputDevice = inputDevice;
lifecycle.addObserver(this);
}
@Override
public boolean isAvailable() {
return mInputDevice != null && mInputDevice.supportsSource(InputDevice.SOURCE_STYLUS);
}
@Nullable
private Preference createDefaultNotesPreference() {
RoleManager rm = mContext.getSystemService(RoleManager.class);
if (rm == null) {
return null;
}
// TODO(b/254834764): replace with notes role once merged
List<String> roleHolders = rm.getRoleHoldersAsUser(RoleManager.ROLE_ASSISTANT,
mContext.getUser());
if (roleHolders.isEmpty()) {
return null;
}
String packageName = roleHolders.get(0);
PackageManager pm = mContext.getPackageManager();
String appName = packageName;
try {
ApplicationInfo ai = pm.getApplicationInfo(packageName,
PackageManager.ApplicationInfoFlags.of(0));
appName = ai == null ? packageName : pm.getApplicationLabel(ai).toString();
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Notes role package not found.");
}
Preference pref = new Preference(mContext);
pref.setKey(KEY_DEFAULT_NOTES);
pref.setTitle(mContext.getString(R.string.stylus_default_notes_app));
pref.setIcon(R.drawable.ic_article);
pref.setEnabled(true);
pref.setSummary(appName);
return pref;
}
private SwitchPreference createHandwritingPreference() {
SwitchPreference pref = new SwitchPreference(mContext);
pref.setKey(KEY_HANDWRITING);
pref.setTitle(mContext.getString(R.string.stylus_textfield_handwriting));
pref.setIcon(R.drawable.ic_text_fields_alt);
pref.setOnPreferenceClickListener(this);
pref.setChecked(Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.STYLUS_HANDWRITING_ENABLED, 0) == 1);
return pref;
}
private SwitchPreference createButtonPressPreference() {
SwitchPreference pref = new SwitchPreference(mContext);
pref.setKey(KEY_IGNORE_BUTTON);
pref.setTitle(mContext.getString(R.string.stylus_ignore_button));
pref.setIcon(R.drawable.ic_block);
pref.setOnPreferenceClickListener(this);
return pref;
}
@Override
public boolean onPreferenceClick(Preference preference) {
String key = preference.getKey();
switch (key) {
case KEY_DEFAULT_NOTES:
PackageManager pm = mContext.getPackageManager();
String packageName = pm.getPermissionControllerPackageName();
// TODO(b/254834764): replace with notes role once merged
Intent intent = new Intent(Intent.ACTION_MANAGE_DEFAULT_APP).setPackage(
packageName).putExtra(Intent.EXTRA_ROLE_NAME, RoleManager.ROLE_ASSISTANT);
mContext.startActivity(intent);
break;
case KEY_HANDWRITING:
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.STYLUS_HANDWRITING_ENABLED,
((SwitchPreference) preference).isChecked() ? 1 : 0);
break;
case KEY_IGNORE_BUTTON:
// TODO(b/251199452): to turn off stylus button presses
break;
}
return true;
}
@Override
public final void displayPreference(PreferenceScreen screen) {
mPreferencesContainer = (PreferenceCategory) screen.findPreference(getPreferenceKey());
super.displayPreference(screen);
refresh();
}
@Override
public String getPreferenceKey() {
return KEY_STYLUS;
}
@Override
public void onResume() {
refresh();
}
private void refresh() {
if (!isAvailable()) return;
if (mInputDevice.getBluetoothAddress() != null) {
Preference notesPref = mPreferencesContainer.findPreference(KEY_DEFAULT_NOTES);
if (notesPref == null) {
notesPref = createDefaultNotesPreference();
if (notesPref != null) {
mPreferencesContainer.addPreference(notesPref);
}
}
}
Preference handwritingPref = mPreferencesContainer.findPreference(KEY_HANDWRITING);
// TODO(b/255732419): add proper InputMethodInfo conditional to show or hide
// InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
if (handwritingPref == null) {
mPreferencesContainer.addPreference(createHandwritingPreference());
}
Preference buttonPref = mPreferencesContainer.findPreference(KEY_IGNORE_BUTTON);
if (buttonPref == null) {
mPreferencesContainer.addPreference(createButtonPressPreference());
}
}
}

View File

@@ -25,16 +25,21 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.hardware.input.InputManager;
import android.os.Bundle;
import android.os.UserManager;
import android.util.FeatureFlagUtils;
import android.view.InputDevice;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.PreferenceScreen;
@@ -51,6 +56,7 @@ import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
@@ -68,6 +74,7 @@ public class BluetoothDeviceDetailsFragmentTest {
private RoboMenu mMenu;
private MenuInflater mInflater;
private FragmentTransaction mFragmentTransaction;
private FragmentActivity mActivity;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private CachedBluetoothDevice mCachedDevice;
@@ -77,29 +84,18 @@ public class BluetoothDeviceDetailsFragmentTest {
private PreferenceScreen mPreferenceScreen;
@Mock
private UserManager mUserManager;
@Mock
private InputManager mInputManager;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
doReturn(mInputManager).when(mContext).getSystemService(InputManager.class);
removeInputDeviceWithMatchingBluetoothAddress();
FakeFeatureFactory.setupForTest();
mFragment = spy(BluetoothDeviceDetailsFragment.newInstance(TEST_ADDRESS));
doReturn(mLocalManager).when(mFragment).getLocalBluetoothManager(any());
doReturn(mCachedDevice).when(mFragment).getCachedDevice(any());
doReturn(mPreferenceScreen).when(mFragment).getPreferenceScreen();
doReturn(mUserManager).when(mFragment).getUserManager();
FragmentManager fragmentManager = mock(FragmentManager.class);
when(mFragment.getFragmentManager()).thenReturn(fragmentManager);
mFragmentTransaction = mock(FragmentTransaction.class);
when(fragmentManager.beginTransaction()).thenReturn(mFragmentTransaction);
when(mCachedDevice.getAddress()).thenReturn(TEST_ADDRESS);
when(mCachedDevice.getIdentityAddress()).thenReturn(TEST_ADDRESS);
Bundle args = new Bundle();
args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS, TEST_ADDRESS);
mFragment.setArguments(args);
mFragment = setupFragment();
mFragment.onAttach(mContext);
mMenu = new RoboMenu(mContext);
@@ -111,6 +107,43 @@ public class BluetoothDeviceDetailsFragmentTest {
assertThat(mFragment.mDeviceAddress).isEqualTo(TEST_ADDRESS);
assertThat(mFragment.mManager).isEqualTo(mLocalManager);
assertThat(mFragment.mCachedDevice).isEqualTo(mCachedDevice);
assertThat(mFragment.mInputDevice).isEqualTo(null);
}
@Test
public void verifyOnAttachResult_flagEnabledAndInputDeviceSet_returnsInputDevice() {
FeatureFlagUtils.setEnabled(mContext, FeatureFlagUtils.SETTINGS_SHOW_STYLUS_PREFERENCES,
true);
InputDevice inputDevice = createInputDeviceWithMatchingBluetoothAddress();
BluetoothDeviceDetailsFragment fragment = setupFragment();
FragmentActivity activity = mock(FragmentActivity.class);
doReturn(inputDevice).when(fragment).getInputDevice(any());
doReturn(activity).when(fragment).getActivity();
fragment.onAttach(mContext);
assertThat(fragment.mDeviceAddress).isEqualTo(TEST_ADDRESS);
assertThat(fragment.mManager).isEqualTo(mLocalManager);
assertThat(fragment.mCachedDevice).isEqualTo(mCachedDevice);
assertThat(fragment.mInputDevice).isEqualTo(inputDevice);
}
@Test
public void verifyOnAttachResult_flagDisabled_returnsNullInputDevice() {
FeatureFlagUtils.setEnabled(mContext, FeatureFlagUtils.SETTINGS_SHOW_STYLUS_PREFERENCES,
false);
InputDevice inputDevice = createInputDeviceWithMatchingBluetoothAddress();
BluetoothDeviceDetailsFragment fragment = setupFragment();
FragmentActivity activity = mock(FragmentActivity.class);
doReturn(inputDevice).when(fragment).getInputDevice(any());
doReturn(activity).when(fragment).getActivity();
fragment.onAttach(mContext);
assertThat(fragment.mDeviceAddress).isEqualTo(TEST_ADDRESS);
assertThat(fragment.mManager).isEqualTo(mLocalManager);
assertThat(fragment.mCachedDevice).isEqualTo(mCachedDevice);
assertThat(fragment.mInputDevice).isEqualTo(null);
}
@Test
@@ -122,6 +155,31 @@ public class BluetoothDeviceDetailsFragmentTest {
assertThat(item.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_rename_button));
}
@Test
public void getTitle_inputDeviceTitle() {
FeatureFlagUtils.setEnabled(mContext, FeatureFlagUtils.SETTINGS_SHOW_STYLUS_PREFERENCES,
true);
doReturn(mock(InputDevice.class)).when(mFragment).getInputDevice(mContext);
mFragment.onAttach(mContext);
mFragment.setTitleForInputDevice();
assertThat(mActivity.getTitle().toString()).isEqualTo(
mContext.getString(R.string.stylus_device_details_title));
}
@Test
public void getTitle_inputDeviceNull_doesNotSetTitle() {
FeatureFlagUtils.setEnabled(mContext, FeatureFlagUtils.SETTINGS_SHOW_STYLUS_PREFERENCES,
true);
doReturn(null).when(mFragment).getInputDevice(mContext);
mFragment.onAttach(mContext);
mFragment.setTitleForInputDevice();
verify(mActivity, times(0)).setTitle(any());
}
@Test
public void editMenu_clicked_showDialog() {
mFragment.onCreateOptionsMenu(mMenu, mInflater);
@@ -131,7 +189,7 @@ public class BluetoothDeviceDetailsFragmentTest {
mFragment.onOptionsItemSelected(item);
assertThat(item.getItemId())
.isEqualTo(BluetoothDeviceDetailsFragment.EDIT_DEVICE_NAME_ITEM_ID);
.isEqualTo(BluetoothDeviceDetailsFragment.EDIT_DEVICE_NAME_ITEM_ID);
verify(mFragmentTransaction).add(captor.capture(), eq(RemoteDeviceNameDialogFragment.TAG));
RemoteDeviceNameDialogFragment dialog = (RemoteDeviceNameDialogFragment) captor.getValue();
assertThat(dialog).isNotNull();
@@ -145,4 +203,44 @@ public class BluetoothDeviceDetailsFragmentTest {
verify(mFragment).finish();
}
private InputDevice createInputDeviceWithMatchingBluetoothAddress() {
doReturn(new int[]{0}).when(mInputManager).getInputDeviceIds();
InputDevice device = mock(InputDevice.class);
doReturn(TEST_ADDRESS).when(mInputManager).getInputDeviceBluetoothAddress(0);
doReturn(device).when(mInputManager).getInputDevice(0);
return device;
}
private InputDevice removeInputDeviceWithMatchingBluetoothAddress() {
doReturn(new int[]{0}).when(mInputManager).getInputDeviceIds();
doReturn(null).when(mInputManager).getInputDeviceBluetoothAddress(0);
return null;
}
private BluetoothDeviceDetailsFragment setupFragment() {
BluetoothDeviceDetailsFragment fragment = spy(
BluetoothDeviceDetailsFragment.newInstance(TEST_ADDRESS));
doReturn(mLocalManager).when(fragment).getLocalBluetoothManager(any());
doReturn(mCachedDevice).when(fragment).getCachedDevice(any());
doReturn(mPreferenceScreen).when(fragment).getPreferenceScreen();
doReturn(mUserManager).when(fragment).getUserManager();
mActivity = spy(Robolectric.setupActivity(FragmentActivity.class));
doReturn(mActivity).when(fragment).getActivity();
doReturn(mContext).when(fragment).getContext();
FragmentManager fragmentManager = mock(FragmentManager.class);
doReturn(fragmentManager).when(fragment).getFragmentManager();
mFragmentTransaction = mock(FragmentTransaction.class);
doReturn(mFragmentTransaction).when(fragmentManager).beginTransaction();
doReturn(TEST_ADDRESS).when(mCachedDevice).getAddress();
doReturn(TEST_ADDRESS).when(mCachedDevice).getIdentityAddress();
Bundle args = new Bundle();
args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS, TEST_ADDRESS);
fragment.setArguments(args);
return fragment;
}
}

View File

@@ -0,0 +1,233 @@
/*
* Copyright (C) 2022 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.settings.connecteddevice.stylus;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.role.RoleManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.provider.Settings;
import android.view.InputDevice;
import android.view.inputmethod.InputMethodManager;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.R;
import com.android.settingslib.core.lifecycle.Lifecycle;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import java.util.Collections;
@RunWith(RobolectricTestRunner.class)
public class StylusDevicesControllerTest {
private static final String NOTES_PACKAGE_NAME = "notes.package";
private static final CharSequence NOTES_APP_LABEL = "App Label";
private Context mContext;
private StylusDevicesController mController;
private PreferenceCategory mPreferenceContainer;
private PreferenceScreen mScreen;
private InputDevice mInputDevice;
@Mock
private InputMethodManager mImm;
@Mock
private PackageManager mPm;
@Mock
private RoleManager mRm;
@Mock
private Lifecycle mLifecycle;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mContext = spy(ApplicationProvider.getApplicationContext());
PreferenceManager preferenceManager = new PreferenceManager(mContext);
mScreen = preferenceManager.createPreferenceScreen(mContext);
mPreferenceContainer = new PreferenceCategory(mContext);
mPreferenceContainer.setKey(StylusDevicesController.KEY_STYLUS);
mScreen.addPreference(mPreferenceContainer);
when(mContext.getSystemService(InputMethodManager.class)).thenReturn(mImm);
when(mContext.getSystemService(RoleManager.class)).thenReturn(mRm);
doNothing().when(mContext).startActivity(any());
// TODO(b/254834764): notes role placeholder
when(mRm.getRoleHoldersAsUser(eq(RoleManager.ROLE_ASSISTANT), any(UserHandle.class)))
.thenReturn(Collections.singletonList(NOTES_PACKAGE_NAME));
when(mContext.getPackageManager()).thenReturn(mPm);
when(mPm.getApplicationInfo(eq(NOTES_PACKAGE_NAME),
any(PackageManager.ApplicationInfoFlags.class))).thenReturn(new ApplicationInfo());
when(mPm.getApplicationLabel(any(ApplicationInfo.class))).thenReturn(NOTES_APP_LABEL);
mInputDevice = spy(new InputDevice.Builder()
.setId(1)
.setSources(InputDevice.SOURCE_STYLUS)
.build());
when(mInputDevice.getBluetoothAddress()).thenReturn("SOME:ADDRESS");
mController = new StylusDevicesController(mContext, mInputDevice, mLifecycle);
}
@Test
public void noInputDevice_noPreference() {
StylusDevicesController controller = new StylusDevicesController(
mContext, null, mLifecycle
);
showScreen(controller);
assertThat(mPreferenceContainer.getPreferenceCount()).isEqualTo(0);
}
@Test
public void btStylusInputDevice_showsAllPreferences() {
showScreen(mController);
Preference defaultNotesPref = mPreferenceContainer.getPreference(0);
Preference handwritingPref = mPreferenceContainer.getPreference(1);
Preference buttonPref = mPreferenceContainer.getPreference(2);
assertThat(mPreferenceContainer.getPreferenceCount()).isEqualTo(3);
assertThat(defaultNotesPref.getTitle().toString()).isEqualTo(
mContext.getString(R.string.stylus_default_notes_app));
assertThat(handwritingPref.getTitle().toString()).isEqualTo(
mContext.getString(R.string.stylus_textfield_handwriting));
assertThat(buttonPref.getTitle().toString()).isEqualTo(
mContext.getString(R.string.stylus_ignore_button));
}
@Test
@Ignore // TODO(b/255732419): unignore when InputMethodInfo available
public void btStylusInputDevice_noHandwritingIme_showsSomePreferences() {
showScreen(mController);
Preference defaultNotesPref = mPreferenceContainer.getPreference(0);
Preference buttonPref = mPreferenceContainer.getPreference(1);
assertThat(mPreferenceContainer.getPreferenceCount()).isEqualTo(2);
assertThat(defaultNotesPref.getTitle().toString()).isEqualTo(
mContext.getString(R.string.stylus_default_notes_app));
assertThat(buttonPref.getTitle().toString()).isEqualTo(
mContext.getString(R.string.stylus_ignore_button));
}
@Test
public void defaultNotesPreference_showsNotesRoleApp() {
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(NOTES_APP_LABEL.toString());
}
@Test
public void defaultNotesPreference_noRoleHolder_hidesNotesRoleApp() {
// TODO(b/254834764): replace with notes role once merged
when(mRm.getRoleHoldersAsUser(eq(RoleManager.ROLE_ASSISTANT), any(UserHandle.class)))
.thenReturn(Collections.emptyList());
showScreen(mController);
for (int i = 0; i < mPreferenceContainer.getPreferenceCount(); i++) {
Preference pref = mPreferenceContainer.getPreference(i);
assertThat(pref.getTitle().toString()).isNotEqualTo(
mContext.getString(R.string.stylus_default_notes_app));
}
}
@Test
public void defaultNotesPreferenceClick_sendsManageDefaultRoleIntent() {
final String permissionPackageName = "permissions.package";
when(mPm.getPermissionControllerPackageName()).thenReturn(permissionPackageName);
final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
showScreen(mController);
Preference defaultNotesPref = mPreferenceContainer.getPreference(0);
mController.onPreferenceClick(defaultNotesPref);
verify(mContext).startActivity(captor.capture());
Intent intent = captor.getValue();
assertThat(intent.getAction()).isEqualTo(Intent.ACTION_MANAGE_DEFAULT_APP);
assertThat(intent.getPackage()).isEqualTo(permissionPackageName);
// TODO(b/254834764): when notes role is merged
assertThat(intent.getStringExtra(Intent.EXTRA_ROLE_NAME)).isEqualTo(
RoleManager.ROLE_ASSISTANT);
}
@Test
public void handwritingPreference_checkedWhenFlagTrue() {
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.STYLUS_HANDWRITING_ENABLED, 1);
showScreen(mController);
SwitchPreference handwritingPref = (SwitchPreference) mPreferenceContainer.getPreference(1);
assertThat(handwritingPref.isChecked()).isEqualTo(true);
}
@Test
public void handwritingPreference_uncheckedWhenFlagFalse() {
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.STYLUS_HANDWRITING_ENABLED, 0);
showScreen(mController);
SwitchPreference handwritingPref = (SwitchPreference) mPreferenceContainer.getPreference(1);
assertThat(handwritingPref.isChecked()).isEqualTo(false);
}
@Test
public void handwritingPreference_updatesFlagOnClick() {
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.STYLUS_HANDWRITING_ENABLED, 0);
showScreen(mController);
SwitchPreference handwritingPref = (SwitchPreference) mPreferenceContainer.getPreference(1);
handwritingPref.performClick();
assertThat(handwritingPref.isChecked()).isEqualTo(true);
assertThat(Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.STYLUS_HANDWRITING_ENABLED, -1)).isEqualTo(1);
}
private void showScreen(StylusDevicesController controller) {
controller.displayPreference(mScreen);
}
}