diff --git a/res/values/strings.xml b/res/values/strings.xml index 3d73571e060..fa1dd362cd3 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -256,8 +256,8 @@ Stylus writing in textfields Ignore all stylus button presses - - USI stylus + + Stylus Date & time diff --git a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java index 61195c99c83..0d51ebed6ab 100644 --- a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java +++ b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java @@ -17,6 +17,9 @@ package com.android.settings.connecteddevice; import android.content.Context; import android.content.pm.PackageManager; +import android.hardware.input.InputManager; +import android.util.FeatureFlagUtils; +import android.view.InputDevice; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; @@ -26,6 +29,7 @@ import androidx.preference.PreferenceScreen; import com.android.settings.bluetooth.BluetoothDeviceUpdater; import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater; import com.android.settings.connecteddevice.dock.DockUpdater; +import com.android.settings.connecteddevice.stylus.StylusDeviceUpdater; import com.android.settings.connecteddevice.usb.ConnectedUsbDeviceUpdater; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerMixin; @@ -51,11 +55,14 @@ public class ConnectedDeviceGroupController extends BasePreferenceController private BluetoothDeviceUpdater mBluetoothDeviceUpdater; private ConnectedUsbDeviceUpdater mConnectedUsbDeviceUpdater; private DockUpdater mConnectedDockUpdater; + private StylusDeviceUpdater mStylusDeviceUpdater; private final PackageManager mPackageManager; + private final InputManager mInputManager; public ConnectedDeviceGroupController(Context context) { super(context, KEY); mPackageManager = context.getPackageManager(); + mInputManager = context.getSystemService(InputManager.class); } @Override @@ -69,7 +76,13 @@ public class ConnectedDeviceGroupController extends BasePreferenceController mConnectedUsbDeviceUpdater.registerCallback(); } - mConnectedDockUpdater.registerCallback(); + if (mConnectedDockUpdater != null) { + mConnectedDockUpdater.registerCallback(); + } + + if (mStylusDeviceUpdater != null) { + mStylusDeviceUpdater.registerCallback(); + } } @Override @@ -82,7 +95,13 @@ public class ConnectedDeviceGroupController extends BasePreferenceController mConnectedUsbDeviceUpdater.unregisterCallback(); } - mConnectedDockUpdater.unregisterCallback(); + if (mConnectedDockUpdater != null) { + mConnectedDockUpdater.unregisterCallback(); + } + + if (mStylusDeviceUpdater != null) { + mStylusDeviceUpdater.unregisterCallback(); + } } @Override @@ -103,8 +122,15 @@ public class ConnectedDeviceGroupController extends BasePreferenceController mConnectedUsbDeviceUpdater.initUsbPreference(context); } - mConnectedDockUpdater.setPreferenceContext(context); - mConnectedDockUpdater.forceUpdate(); + if (mConnectedDockUpdater != null) { + mConnectedDockUpdater.setPreferenceContext(context); + mConnectedDockUpdater.forceUpdate(); + } + + if (mStylusDeviceUpdater != null) { + mStylusDeviceUpdater.setPreferenceContext(context); + mStylusDeviceUpdater.forceUpdate(); + } } } @@ -112,6 +138,7 @@ public class ConnectedDeviceGroupController extends BasePreferenceController public int getAvailabilityStatus() { return (hasBluetoothFeature() || hasUsbFeature() + || hasUsiStylusFeature() || mConnectedDockUpdater != null) ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; @@ -141,11 +168,13 @@ public class ConnectedDeviceGroupController extends BasePreferenceController @VisibleForTesting void init(BluetoothDeviceUpdater bluetoothDeviceUpdater, ConnectedUsbDeviceUpdater connectedUsbDeviceUpdater, - DockUpdater connectedDockUpdater) { + DockUpdater connectedDockUpdater, + StylusDeviceUpdater connectedStylusDeviceUpdater) { mBluetoothDeviceUpdater = bluetoothDeviceUpdater; mConnectedUsbDeviceUpdater = connectedUsbDeviceUpdater; mConnectedDockUpdater = connectedDockUpdater; + mStylusDeviceUpdater = connectedStylusDeviceUpdater; } public void init(DashboardFragment fragment) { @@ -160,7 +189,10 @@ public class ConnectedDeviceGroupController extends BasePreferenceController hasUsbFeature() ? new ConnectedUsbDeviceUpdater(context, fragment, this) : null, - connectedDockUpdater); + connectedDockUpdater, + hasUsiStylusFeature() + ? new StylusDeviceUpdater(context, fragment, this) + : null); } private boolean hasBluetoothFeature() { @@ -171,4 +203,21 @@ public class ConnectedDeviceGroupController extends BasePreferenceController return mPackageManager.hasSystemFeature(PackageManager.FEATURE_USB_ACCESSORY) || mPackageManager.hasSystemFeature(PackageManager.FEATURE_USB_HOST); } + + private boolean hasUsiStylusFeature() { + if (!FeatureFlagUtils.isEnabled(mContext, + FeatureFlagUtils.SETTINGS_SHOW_STYLUS_PREFERENCES)) { + return false; + } + + for (int deviceId : mInputManager.getInputDeviceIds()) { + InputDevice device = mInputManager.getInputDevice(deviceId); + if (device != null + && device.supportsSource(InputDevice.SOURCE_STYLUS) + && !device.isExternal()) { + return true; + } + } + return false; + } } diff --git a/src/com/android/settings/connecteddevice/stylus/StylusDeviceUpdater.java b/src/com/android/settings/connecteddevice/stylus/StylusDeviceUpdater.java new file mode 100644 index 00000000000..575c526c2dd --- /dev/null +++ b/src/com/android/settings/connecteddevice/stylus/StylusDeviceUpdater.java @@ -0,0 +1,225 @@ +/* + * 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.content.Context; +import android.hardware.BatteryState; +import android.hardware.input.InputManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; +import android.view.InputDevice; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.connecteddevice.DevicePreferenceCallback; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * Controller to maintain available USI stylus devices. Listens to bluetooth + * stylus connection to determine whether to show the USI preference. + */ +public class StylusDeviceUpdater implements InputManager.InputDeviceListener, + InputManager.InputDeviceBatteryListener { + + private static final String TAG = "StylusDeviceUpdater"; + private static final String PREF_KEY = "stylus_usi_device"; + private static final String INPUT_ID_ARG = "device_input_id"; + + private final DevicePreferenceCallback mDevicePreferenceCallback; + private final List mRegisteredBatteryCallbackIds; + private final DashboardFragment mFragment; + private final InputManager mInputManager; + private final MetricsFeatureProvider mMetricsFeatureProvider; + + private long mLastUsiSeenTime = 0; + private Context mContext; + + @VisibleForTesting + Integer mLastDetectedUsiId; + + @VisibleForTesting + Preference mUsiPreference; + + + public StylusDeviceUpdater(Context context, DashboardFragment fragment, + DevicePreferenceCallback devicePreferenceCallback) { + mFragment = fragment; + mRegisteredBatteryCallbackIds = new ArrayList<>(); + mDevicePreferenceCallback = devicePreferenceCallback; + mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider(); + mContext = context; + mInputManager = context.getSystemService(InputManager.class); + + } + + /** + * Register the stylus event callback and update the list + */ + public void registerCallback() { + for (int deviceId : mInputManager.getInputDeviceIds()) { + onInputDeviceAdded(deviceId); + } + mInputManager.registerInputDeviceListener(this, new Handler(Looper.myLooper())); + forceUpdate(); + } + + /** + * Unregister the stylus event callback + */ + public void unregisterCallback() { + for (int deviceId : mRegisteredBatteryCallbackIds) { + mInputManager.removeInputDeviceBatteryListener(deviceId, this); + } + mInputManager.unregisterInputDeviceListener(this); + } + + @Override + public void onInputDeviceAdded(int deviceId) { + InputDevice inputDevice = mInputManager.getInputDevice(deviceId); + if (inputDevice.supportsSource(InputDevice.SOURCE_STYLUS) + && !inputDevice.isExternal()) { + try { + mInputManager.addInputDeviceBatteryListener(deviceId, + mContext.getMainExecutor(), this); + mRegisteredBatteryCallbackIds.add(deviceId); + } catch (IllegalArgumentException e) { + Log.e(TAG, e.getMessage()); + } + } + forceUpdate(); + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + Log.d(TAG, String.format("Input device removed %d", deviceId)); + forceUpdate(); + } + + @Override + public void onInputDeviceChanged(int deviceId) { + if (mInputManager.getInputDevice(deviceId).supportsSource(InputDevice.SOURCE_STYLUS)) { + forceUpdate(); + } + } + + + @Override + public void onBatteryStateChanged(int deviceId, long eventTimeMillis, + @NonNull BatteryState batteryState) { + if (batteryState.isPresent()) { + mLastUsiSeenTime = eventTimeMillis; + mLastDetectedUsiId = deviceId; + } else { + mLastUsiSeenTime = -1; + mLastDetectedUsiId = null; + } + forceUpdate(); + } + + /** + * Set the context to generate the {@link Preference}, so it could get the correct theme. + */ + public void setPreferenceContext(Context context) { + mContext = context; + } + + /** + * Force update to add or remove stylus preference + */ + public void forceUpdate() { + if (shouldShowUsiPreference()) { + addOrUpdateUsiPreference(); + } else { + removeUsiPreference(); + } + } + + private synchronized void addOrUpdateUsiPreference() { + if (mUsiPreference == null) { + mUsiPreference = new Preference(mContext); + mDevicePreferenceCallback.onDeviceAdded(mUsiPreference); + } + mUsiPreference.setKey(PREF_KEY); + mUsiPreference.setTitle(R.string.stylus_connected_devices_title); + // TODO(b/250909304): pending actual icon visD + mUsiPreference.setIcon(R.drawable.circle); + mUsiPreference.setOnPreferenceClickListener((Preference p) -> { + mMetricsFeatureProvider.logClickedPreference(p, mFragment.getMetricsCategory()); + launchDeviceDetails(); + return true; + }); + } + + private synchronized void removeUsiPreference() { + if (mUsiPreference != null) { + mDevicePreferenceCallback.onDeviceRemoved(mUsiPreference); + mUsiPreference = null; + } + } + + private boolean shouldShowUsiPreference() { + return isUsiConnectionValid() && !hasConnectedBluetoothStylusDevice(); + } + + @VisibleForTesting + public Preference getPreference() { + return mUsiPreference; + } + + @VisibleForTesting + boolean hasConnectedBluetoothStylusDevice() { + for (int deviceId : mInputManager.getInputDeviceIds()) { + InputDevice device = mInputManager.getInputDevice(deviceId); + if (device.supportsSource(InputDevice.SOURCE_STYLUS) + && mInputManager.getInputDeviceBluetoothAddress(deviceId) != null) { + return true; + } + } + + return false; + } + + @VisibleForTesting + boolean isUsiConnectionValid() { + // battery listener uses uptimeMillis as its eventTime + long currentTime = SystemClock.uptimeMillis(); + long usiValidityDuration = 60 * 60 * 1000; // 1 hour + return mLastUsiSeenTime > 0 && currentTime - usiValidityDuration <= mLastUsiSeenTime; + } + + private void launchDeviceDetails() { + final Bundle args = new Bundle(); + args.putInt(INPUT_ID_ARG, mLastDetectedUsiId); + + new SubSettingLauncher(mFragment.getContext()) + .setDestination(StylusUsiDetailsFragment.class.getName()) + .setArguments(args) + .setSourceMetricsCategory(mFragment.getMetricsCategory()).launch(); + } +} diff --git a/src/com/android/settings/connecteddevice/stylus/StylusUsiHeaderController.java b/src/com/android/settings/connecteddevice/stylus/StylusUsiHeaderController.java index 826cc1f6a30..13d7b57f33e 100644 --- a/src/com/android/settings/connecteddevice/stylus/StylusUsiHeaderController.java +++ b/src/com/android/settings/connecteddevice/stylus/StylusUsiHeaderController.java @@ -66,7 +66,7 @@ public class StylusUsiHeaderController extends BasePreferenceController implemen mHeaderPreference = screen.findPreference(getPreferenceKey()); View view = mHeaderPreference.findViewById(R.id.entity_header); TextView titleView = view.findViewById(R.id.entity_header_title); - titleView.setText(R.string.stylus_usi_header_title); + titleView.setText(R.string.stylus_connected_devices_title); ImageView iconView = mHeaderPreference.findViewById(R.id.entity_header_icon); if (iconView != null) { diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java index fa72fba2c05..db23c43857d 100644 --- a/src/com/android/settings/core/gateway/SettingsGateway.java +++ b/src/com/android/settings/core/gateway/SettingsGateway.java @@ -83,6 +83,7 @@ import com.android.settings.connecteddevice.AdvancedConnectedDeviceDashboardFrag import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment; import com.android.settings.connecteddevice.NfcAndPaymentFragment; import com.android.settings.connecteddevice.PreviouslyConnectedDeviceDashboardFragment; +import com.android.settings.connecteddevice.stylus.StylusUsiDetailsFragment; import com.android.settings.connecteddevice.usb.UsbDetailsFragment; import com.android.settings.datausage.DataSaverSummary; import com.android.settings.datausage.DataUsageList; @@ -338,6 +339,7 @@ public class SettingsGateway { BluetoothDeviceDetailsFragment.class.getName(), BluetoothBroadcastDialog.class.getName(), BluetoothFindBroadcastsFragment.class.getName(), + StylusUsiDetailsFragment.class.getName(), DataUsageList.class.getName(), ToggleBackupSettingFragment.class.getName(), PreviouslyConnectedDeviceDashboardFragment.class.getName(), diff --git a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java index 40e6494522d..a35ef45f518 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java @@ -29,6 +29,9 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.pm.PackageManager; +import android.hardware.input.InputManager; +import android.util.FeatureFlagUtils; +import android.view.InputDevice; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; @@ -37,6 +40,7 @@ import androidx.preference.PreferenceScreen; import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater; import com.android.settings.connecteddevice.dock.DockUpdater; +import com.android.settings.connecteddevice.stylus.StylusDeviceUpdater; import com.android.settings.connecteddevice.usb.ConnectedUsbDeviceUpdater; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; @@ -68,9 +72,13 @@ public class ConnectedDeviceGroupControllerTest { @Mock private DockUpdater mConnectedDockUpdater; @Mock + private StylusDeviceUpdater mStylusDeviceUpdater; + @Mock private PreferenceScreen mPreferenceScreen; @Mock(answer = Answers.RETURNS_DEEP_STUBS) private PreferenceManager mPreferenceManager; + @Mock + private InputManager mInputManager; private ShadowApplicationPackageManager mPackageManager; private PreferenceGroup mPreferenceGroup; @@ -82,7 +90,7 @@ public class ConnectedDeviceGroupControllerTest { public void setUp() { MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; + mContext = spy(RuntimeEnvironment.application); mPreference = new Preference(mContext); mPreference.setKey(PREFERENCE_KEY_1); mPackageManager = (ShadowApplicationPackageManager) Shadows.shadowOf( @@ -91,11 +99,16 @@ public class ConnectedDeviceGroupControllerTest { when(mPreferenceGroup.getPreferenceManager()).thenReturn(mPreferenceManager); doReturn(mContext).when(mDashboardFragment).getContext(); mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, true); + when(mContext.getSystemService(InputManager.class)).thenReturn(mInputManager); + when(mInputManager.getInputDeviceIds()).thenReturn(new int[]{}); mConnectedDeviceGroupController = new ConnectedDeviceGroupController(mContext); mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, - mConnectedUsbDeviceUpdater, mConnectedDockUpdater); + mConnectedUsbDeviceUpdater, mConnectedDockUpdater, mStylusDeviceUpdater); mConnectedDeviceGroupController.mPreferenceGroup = mPreferenceGroup; + + FeatureFlagUtils.setEnabled(mContext, FeatureFlagUtils.SETTINGS_SHOW_STYLUS_PREFERENCES, + true); } @Test @@ -146,6 +159,7 @@ public class ConnectedDeviceGroupControllerTest { verify(mConnectedUsbDeviceUpdater).registerCallback(); verify(mConnectedDockUpdater).registerCallback(); verify(mConnectedBluetoothDeviceUpdater).refreshPreference(); + verify(mStylusDeviceUpdater).registerCallback(); } @Test @@ -155,6 +169,7 @@ public class ConnectedDeviceGroupControllerTest { verify(mConnectedBluetoothDeviceUpdater).unregisterCallback(); verify(mConnectedUsbDeviceUpdater).unregisterCallback(); verify(mConnectedDockUpdater).unregisterCallback(); + verify(mStylusDeviceUpdater).unregisterCallback(); } @Test @@ -163,7 +178,7 @@ public class ConnectedDeviceGroupControllerTest { mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, - mConnectedUsbDeviceUpdater, null); + mConnectedUsbDeviceUpdater, null, null); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( UNSUPPORTED_ON_DEVICE); @@ -175,7 +190,7 @@ public class ConnectedDeviceGroupControllerTest { mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, - mConnectedUsbDeviceUpdater, null); + mConnectedUsbDeviceUpdater, null, null); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( AVAILABLE_UNSEARCHABLE); @@ -187,7 +202,7 @@ public class ConnectedDeviceGroupControllerTest { mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, true); mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, - mConnectedUsbDeviceUpdater, null); + mConnectedUsbDeviceUpdater, null, null); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( AVAILABLE_UNSEARCHABLE); @@ -199,7 +214,40 @@ public class ConnectedDeviceGroupControllerTest { mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, - mConnectedUsbDeviceUpdater, mConnectedDockUpdater); + mConnectedUsbDeviceUpdater, mConnectedDockUpdater, null); + + assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( + AVAILABLE_UNSEARCHABLE); + } + + + @Test + public void getAvailabilityStatus_noUsiStylusFeature_returnUnSupported() { + mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); + mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); + mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); + when(mInputManager.getInputDeviceIds()).thenReturn(new int[]{0}); + when(mInputManager.getInputDevice(0)).thenReturn(new InputDevice.Builder().setSources( + InputDevice.SOURCE_DPAD).setExternal(false).build()); + + mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedUsbDeviceUpdater, null, mStylusDeviceUpdater); + + assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( + UNSUPPORTED_ON_DEVICE); + } + + @Test + public void getAvailabilityStatus_haveUsiStylusFeature_returnSupported() { + mPackageManager.setSystemFeature(PackageManager.FEATURE_BLUETOOTH, false); + mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_ACCESSORY, false); + mPackageManager.setSystemFeature(PackageManager.FEATURE_USB_HOST, false); + when(mInputManager.getInputDeviceIds()).thenReturn(new int[]{0}); + when(mInputManager.getInputDevice(0)).thenReturn(new InputDevice.Builder().setSources( + InputDevice.SOURCE_STYLUS).setExternal(false).build()); + + mConnectedDeviceGroupController.init(mConnectedBluetoothDeviceUpdater, + mConnectedUsbDeviceUpdater, mConnectedDockUpdater, mStylusDeviceUpdater); assertThat(mConnectedDeviceGroupController.getAvailabilityStatus()).isEqualTo( AVAILABLE_UNSEARCHABLE); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDeviceUpdaterTest.java b/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDeviceUpdaterTest.java new file mode 100644 index 00000000000..10f37275712 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDeviceUpdaterTest.java @@ -0,0 +1,268 @@ +/* + * 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.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.content.Intent; +import android.hardware.BatteryState; +import android.hardware.input.InputManager; +import android.os.SystemClock; +import android.view.InputDevice; + +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.connecteddevice.DevicePreferenceCallback; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; + +import org.junit.Before; +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 org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class}) +public class StylusDeviceUpdaterTest { + + private Context mContext; + private StylusDeviceUpdater mStylusDeviceUpdater; + private InputDevice mStylusDevice; + private InputDevice mOtherDevice; + + @Mock + private SettingsActivity mSettingsActivity; + @Mock + private DashboardFragment mDashboardFragment; + @Mock + private DevicePreferenceCallback mDevicePreferenceCallback; + @Mock + private InputManager mInputManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = spy(RuntimeEnvironment.application); + + doReturn(mContext).when(mDashboardFragment).getContext(); + doReturn(mInputManager).when(mContext).getSystemService(InputManager.class); + doReturn(new int[]{}).when(mInputManager).getInputDeviceIds(); + + mStylusDeviceUpdater = spy( + new StylusDeviceUpdater(mContext, mDashboardFragment, mDevicePreferenceCallback)); + mStylusDeviceUpdater.setPreferenceContext(mContext); + + doReturn(new int[]{0, 1}).when(mInputManager).getInputDeviceIds(); + mOtherDevice = new InputDevice.Builder().setId(0).setName("other").setSources( + InputDevice.SOURCE_DPAD).build(); + doReturn(mOtherDevice).when(mInputManager).getInputDevice(0); + mStylusDevice = new InputDevice.Builder().setId(1).setName("Pen").setExternal( + false).setSources( + InputDevice.SOURCE_STYLUS).build(); + doReturn(mStylusDevice).when(mInputManager).getInputDevice(1); + } + + @Test + public void registerCallback_registersBatteryListener() { + mStylusDeviceUpdater.registerCallback(); + + verify(mInputManager, times(1)).addInputDeviceBatteryListener(eq(1), any(), + any()); + } + + @Test + public void registerCallback_registersInputDeviceListener() { + mStylusDeviceUpdater.registerCallback(); + + verify(mInputManager, times(1)).registerInputDeviceListener(eq(mStylusDeviceUpdater), + any()); + } + + @Test + public void onInputDeviceAdded_internalStylus_registersBatteryListener() { + mStylusDeviceUpdater.onInputDeviceAdded(1); + + verify(mInputManager, times(1)).addInputDeviceBatteryListener(eq(1), any(), + any()); + } + + @Test + public void onInputDeviceAdded_nonStylus_doesNotRegisterBatteryListener() { + mStylusDeviceUpdater.onInputDeviceAdded(0); + + verify(mInputManager, never()).addInputDeviceBatteryListener(eq(1), any(), + any()); + } + + @Test + public void click_usiPreference_launchUsiDetailsPage() { + doReturn(mSettingsActivity).when(mDashboardFragment).getContext(); + doReturn(true).when(mStylusDeviceUpdater).isUsiConnectionValid(); + doReturn(false).when(mStylusDeviceUpdater).hasConnectedBluetoothStylusDevice(); + mStylusDeviceUpdater.forceUpdate(); + mStylusDeviceUpdater.mLastDetectedUsiId = 1; + final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + + mStylusDeviceUpdater.mUsiPreference.performClick(); + + assertThat(mStylusDeviceUpdater.mUsiPreference.getTitle().toString()).isEqualTo( + mContext.getString(R.string.stylus_connected_devices_title)); + verify(mSettingsActivity).startActivity(intentCaptor.capture()); + assertThat(intentCaptor.getValue().getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(StylusUsiDetailsFragment.class.getName()); + } + + @Test + public void forceUpdate_addsUsiPreference_validUsiDevice() { + doReturn(true).when(mStylusDeviceUpdater).isUsiConnectionValid(); + doReturn(false).when(mStylusDeviceUpdater).hasConnectedBluetoothStylusDevice(); + + mStylusDeviceUpdater.forceUpdate(); + + assertThat(mStylusDeviceUpdater.mUsiPreference).isNotNull(); + } + + @Test + public void forceUpdate_doesNotAddPreference_invalidUsiDevice() { + doReturn(false).when(mStylusDeviceUpdater).isUsiConnectionValid(); + doReturn(false).when(mStylusDeviceUpdater).hasConnectedBluetoothStylusDevice(); + + mStylusDeviceUpdater.forceUpdate(); + + assertThat(mStylusDeviceUpdater.mUsiPreference).isNull(); + } + + @Test + public void forceUpdate_removesUsiPreference_existingPreference_invalidUsiDevice() { + doReturn(true).when(mStylusDeviceUpdater).isUsiConnectionValid(); + doReturn(false).when(mStylusDeviceUpdater).hasConnectedBluetoothStylusDevice(); + + mStylusDeviceUpdater.forceUpdate(); + + doReturn(false).when(mStylusDeviceUpdater).isUsiConnectionValid(); + mStylusDeviceUpdater.forceUpdate(); + + assertThat(mStylusDeviceUpdater.mUsiPreference).isNull(); + } + + @Test + public void forceUpdate_doesNotAddUsiPreference_bluetoothStylusConnected() { + doReturn(true).when(mStylusDeviceUpdater).isUsiConnectionValid(); + doReturn(true).when(mStylusDeviceUpdater).hasConnectedBluetoothStylusDevice(); + + mStylusDeviceUpdater.forceUpdate(); + + assertThat(mStylusDeviceUpdater.mUsiPreference).isNull(); + } + + @Test + public void forceUpdate_addsUsiPreference_bluetoothStylusDisconnected() { + doReturn(true).when(mStylusDeviceUpdater).isUsiConnectionValid(); + doReturn(true).when(mStylusDeviceUpdater).hasConnectedBluetoothStylusDevice(); + mStylusDeviceUpdater.forceUpdate(); + + doReturn(false).when(mStylusDeviceUpdater).hasConnectedBluetoothStylusDevice(); + mStylusDeviceUpdater.forceUpdate(); + + assertThat(mStylusDeviceUpdater.mUsiPreference).isNotNull(); + } + + @Test + public void forceUpdate_removesUsiPreference_existingPreference_bluetoothStylusConnected() { + doReturn(true).when(mStylusDeviceUpdater).isUsiConnectionValid(); + doReturn(false).when(mStylusDeviceUpdater).hasConnectedBluetoothStylusDevice(); + mStylusDeviceUpdater.forceUpdate(); + doReturn(true).when(mStylusDeviceUpdater).hasConnectedBluetoothStylusDevice(); + + mStylusDeviceUpdater.forceUpdate(); + + assertThat(mStylusDeviceUpdater.mUsiPreference).isNull(); + } + + @Test + public void onBatteryStateChanged_detectsValidUsi() { + BatteryState batteryState = mock(BatteryState.class); + doReturn(true).when(batteryState).isPresent(); + doReturn(0.5f).when(batteryState).getCapacity(); + + mStylusDeviceUpdater.onBatteryStateChanged(1, SystemClock.uptimeMillis(), + batteryState); + + assertThat(mStylusDeviceUpdater.isUsiConnectionValid()).isTrue(); + } + + @Test + public void onBatteryStateChanged_detectsInvalidUsi_batteryNotPresent() { + doReturn(false).when(mStylusDeviceUpdater).hasConnectedBluetoothStylusDevice(); + BatteryState batteryState = mock(BatteryState.class); + doReturn(false).when(batteryState).isPresent(); + + mStylusDeviceUpdater.onBatteryStateChanged(1, SystemClock.uptimeMillis(), + batteryState); + + assertThat(mStylusDeviceUpdater.isUsiConnectionValid()).isFalse(); + } + + @Test + public void onBatteryStateChanged_ddetectsInvalidUsi_staleBatteryEventTime() { + doReturn(false).when(mStylusDeviceUpdater).hasConnectedBluetoothStylusDevice(); + BatteryState batteryState = mock(BatteryState.class); + doReturn(true).when(batteryState).isPresent(); + doReturn(0.5f).when(batteryState).getCapacity(); + + mStylusDeviceUpdater.onBatteryStateChanged(1, 0, batteryState); + + assertThat(mStylusDeviceUpdater.isUsiConnectionValid()).isFalse(); + } + + @Test + public void detectsConnectedBluetoothStylus() { + InputDevice stylusDevice = new InputDevice.Builder().setId(1).setName("Pen").setSources( + InputDevice.SOURCE_STYLUS) + .build(); + doReturn(stylusDevice).when(mInputManager).getInputDevice(1); + doReturn("04:52:C7:0B:D8:3C").when(mInputManager).getInputDeviceBluetoothAddress(1); + + assertThat(mStylusDeviceUpdater.hasConnectedBluetoothStylusDevice()).isTrue(); + } + + @Test + public void detectsDisconnectedBluetoothStylus() { + InputDevice stylusDevice = new InputDevice.Builder().setId(1).setName("Pen").setSources( + InputDevice.SOURCE_STYLUS).build(); + doReturn(stylusDevice).when(mInputManager).getInputDevice(1); + doReturn(null).when(mInputManager).getInputDeviceBluetoothAddress(1); + + assertThat(mStylusDeviceUpdater.hasConnectedBluetoothStylusDevice()).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusUsiHeaderControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusUsiHeaderControllerTest.java index 27b1de52a11..3aad02e011a 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusUsiHeaderControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusUsiHeaderControllerTest.java @@ -109,7 +109,7 @@ public class StylusUsiHeaderControllerTest { assertThat(((TextView) mLayoutPreference.findViewById( R.id.entity_header_title)).getText().toString()).isEqualTo( - mContext.getString(R.string.stylus_usi_header_title)); + mContext.getString(R.string.stylus_connected_devices_title)); } @Test