diff --git a/res/values/strings.xml b/res/values/strings.xml index 43827e369e7..793498ebee9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -394,6 +394,11 @@ Pair new device + + Currently connected + + Saved devices + Date & time diff --git a/res/xml/connected_devices.xml b/res/xml/connected_devices.xml index c7f7f484036..d24dd51755c 100644 --- a/res/xml/connected_devices.xml +++ b/res/xml/connected_devices.xml @@ -19,4 +19,7 @@ android:key="connected_devices_screen" android:title="@string/connected_devices_dashboard_title"> + diff --git a/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java b/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java new file mode 100644 index 00000000000..239e4051a4e --- /dev/null +++ b/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdater.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 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.bluetooth; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.support.annotation.VisibleForTesting; + +import com.android.settings.connecteddevice.DevicePreferenceCallback; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +/** + * Controller to maintain connected bluetooth devices + */ +public class ConnectedBluetoothDeviceUpdater extends BluetoothDeviceUpdater { + + public ConnectedBluetoothDeviceUpdater(DashboardFragment fragment, + DevicePreferenceCallback devicePreferenceCallback) { + super(fragment, devicePreferenceCallback); + } + + @VisibleForTesting + ConnectedBluetoothDeviceUpdater(DashboardFragment fragment, + DevicePreferenceCallback devicePreferenceCallback, + LocalBluetoothManager localBluetoothManager) { + super(fragment, devicePreferenceCallback, localBluetoothManager); + } + + @Override + public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { + if (state == BluetoothAdapter.STATE_CONNECTED) { + addPreference(cachedDevice); + } else if (state == BluetoothAdapter.STATE_DISCONNECTED) { + removePreference(cachedDevice); + } + } + + @Override + public void update(CachedBluetoothDevice cachedDevice) { + final BluetoothDevice device = cachedDevice.getDevice(); + final boolean filterMatch = + device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected(); + + if (filterMatch) { + // Add the preference if it is new one + addPreference(cachedDevice); + } else { + removePreference(cachedDevice); + } + } +} diff --git a/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragment.java b/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragment.java index 74f6ea2215a..14acd89a60a 100644 --- a/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragment.java +++ b/src/com/android/settings/connecteddevice/ConnectedDeviceDashboardFragment.java @@ -17,27 +17,20 @@ package com.android.settings.connecteddevice; import android.app.Activity; import android.content.Context; -import android.content.pm.PackageManager; import android.provider.SearchIndexableResource; import android.support.annotation.VisibleForTesting; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; -import com.android.settings.SettingsActivity; -import com.android.settings.bluetooth.BluetoothMasterSwitchPreferenceController; -import com.android.settings.bluetooth.Utils; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.SummaryLoader; -import com.android.settings.deviceinfo.UsbBackend; import com.android.settings.nfc.NfcPreferenceController; -import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; public class ConnectedDeviceDashboardFragment extends DashboardFragment { @@ -66,7 +59,14 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment { @Override protected List getPreferenceControllers(Context context) { - return null; + final List controllers = new ArrayList<>(); + final Lifecycle lifecycle = getLifecycle(); + + final ConnectedDeviceGroupController connectedDeviceGroupController = + new ConnectedDeviceGroupController(this, lifecycle); + controllers.add(connectedDeviceGroupController); + return controllers; + } @VisibleForTesting @@ -109,19 +109,26 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment { /** * For Search. */ + //TODO(b/69333961): update the index for this new fragment public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new BaseSearchIndexProvider() { @Override public List getXmlResourcesToIndex( Context context, boolean enabled) { - return new ArrayList<>(); + return new ArrayList<>(); } @Override public List getNonIndexableKeys(Context context) { - return new ArrayList<>(); } + + @Override + public List getPreferenceControllers( + Context context) { + //TODO(b/69333961): update the index for controllers + return super.getPreferenceControllers(context); + } }; } diff --git a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java index 0eab26d45ae..a0b5cb85697 100644 --- a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java +++ b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java @@ -15,64 +15,95 @@ */ package com.android.settings.connecteddevice; -import android.content.Context; +import android.support.annotation.VisibleForTesting; import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceGroup; import android.support.v7.preference.PreferenceScreen; - import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.bluetooth.BluetoothDeviceUpdater; +import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.core.lifecycle.events.OnStart; import com.android.settingslib.core.lifecycle.events.OnStop; /** * Controller to maintain the {@link android.support.v7.preference.PreferenceGroup} for all * connected devices. It uses {@link DevicePreferenceCallback} to add/remove {@link Preference} - * - * TODO(b/69333961): add real impl */ public class ConnectedDeviceGroupController extends AbstractPreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnStart, OnStop, DevicePreferenceCallback { - private BluetoothDeviceUpdater mBluetoothController; - public ConnectedDeviceGroupController(Context context) { - super(context); + private static final String KEY = "connected_device_list"; + + @VisibleForTesting + PreferenceGroup mPreferenceGroup; + private BluetoothDeviceUpdater mBluetoothDeviceUpdater; + + public ConnectedDeviceGroupController(DashboardFragment fragment, Lifecycle lifecycle) { + super(fragment.getContext()); + init(lifecycle, new ConnectedBluetoothDeviceUpdater(fragment, this)); + } + + @VisibleForTesting + ConnectedDeviceGroupController(DashboardFragment fragment, Lifecycle lifecycle, + BluetoothDeviceUpdater bluetoothDeviceUpdater) { + super(fragment.getContext()); + init(lifecycle, bluetoothDeviceUpdater); } @Override public void onStart() { - mBluetoothController.registerCallback(); + mBluetoothDeviceUpdater.registerCallback(); } @Override public void onStop() { - mBluetoothController.unregisterCallback(); + mBluetoothDeviceUpdater.unregisterCallback(); } @Override public void displayPreference(PreferenceScreen screen) { - + super.displayPreference(screen); + mPreferenceGroup = (PreferenceGroup) screen.findPreference(KEY); + mPreferenceGroup.setVisible(false); + mBluetoothDeviceUpdater.setPrefContext(screen.getContext()); + mBluetoothDeviceUpdater.forceUpdate(); } @Override public boolean isAvailable() { - return false; + return true; } @Override public String getPreferenceKey() { - return null; + return KEY; } @Override public void onDeviceAdded(Preference preference) { - + if (mPreferenceGroup.getPreferenceCount() == 0) { + mPreferenceGroup.setVisible(true); + } + mPreferenceGroup.addPreference(preference); } @Override public void onDeviceRemoved(Preference preference) { + mPreferenceGroup.removePreference(preference); + if (mPreferenceGroup.getPreferenceCount() == 0) { + mPreferenceGroup.setVisible(false); + } + } + private void init(Lifecycle lifecycle, BluetoothDeviceUpdater bluetoothDeviceUpdater) { + if (lifecycle != null) { + lifecycle.addObserver(this); + } + mBluetoothDeviceUpdater = bluetoothDeviceUpdater; } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java b/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java new file mode 100644 index 00000000000..c86664c8ee3 --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/ConnectedBluetoothDeviceUpdaterTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2017 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.bluetooth; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.Context; + +import com.android.settings.TestConfig; +import com.android.settings.connecteddevice.DevicePreferenceCallback; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION_O) +public class ConnectedBluetoothDeviceUpdaterTest { + @Mock + private DashboardFragment mDashboardFragment; + @Mock + private DevicePreferenceCallback mDevicePreferenceCallback; + @Mock + private CachedBluetoothDevice mCachedBluetoothDevice; + @Mock + private BluetoothDevice mBluetoothDevice; + + private Context mContext; + private ConnectedBluetoothDeviceUpdater mBluetoothDeviceUpdater; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + doReturn(mContext).when(mDashboardFragment).getContext(); + doReturn(mBluetoothDevice).when(mCachedBluetoothDevice).getDevice(); + + mBluetoothDeviceUpdater = spy(new ConnectedBluetoothDeviceUpdater(mDashboardFragment, + mDevicePreferenceCallback, null)); + mBluetoothDeviceUpdater.setPrefContext(mContext); + doNothing().when(mBluetoothDeviceUpdater).addPreference(any()); + doNothing().when(mBluetoothDeviceUpdater).removePreference(any()); + } + + @Test + public void testUpdate_filterMatch_addPreference() { + doReturn(BluetoothDevice.BOND_BONDED).when(mBluetoothDevice).getBondState(); + doReturn(true).when(mBluetoothDevice).isConnected(); + + mBluetoothDeviceUpdater.update(mCachedBluetoothDevice); + + verify(mBluetoothDeviceUpdater).addPreference(mCachedBluetoothDevice); + } + + @Test + public void testUpdate_filterNotMatch_removePreference() { + doReturn(BluetoothDevice.BOND_NONE).when(mBluetoothDevice).getBondState(); + doReturn(false).when(mBluetoothDevice).isConnected(); + + mBluetoothDeviceUpdater.update(mCachedBluetoothDevice); + + verify(mBluetoothDeviceUpdater).removePreference(mCachedBluetoothDevice); + } + + @Test + public void testOnConnectionStateChanged_deviceConnected_addPreference() { + mBluetoothDeviceUpdater.onConnectionStateChanged(mCachedBluetoothDevice, + BluetoothAdapter.STATE_CONNECTED); + + verify(mBluetoothDeviceUpdater).addPreference(mCachedBluetoothDevice); + } + + @Test + public void testOnConnectionStateChanged_deviceDisconnected_removePreference() { + mBluetoothDeviceUpdater.onConnectionStateChanged(mCachedBluetoothDevice, + BluetoothAdapter.STATE_DISCONNECTED); + + verify(mBluetoothDeviceUpdater).removePreference(mCachedBluetoothDevice); + } + +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java new file mode 100644 index 00000000000..e18115aaa56 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2017 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; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceGroup; +import android.support.v7.preference.PreferenceManager; +import android.support.v7.preference.PreferenceScreen; + +import com.android.settings.TestConfig; +import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION_O) +public class ConnectedDeviceGroupControllerTest { + @Mock + private DashboardFragment mDashboardFragment; + @Mock + private ConnectedBluetoothDeviceUpdater mConnectedBluetoothDeviceUpdater; + @Mock + private PreferenceScreen mPreferenceScreen; + @Mock + private PreferenceManager mPreferenceManager; + + private PreferenceGroup mPreferenceGroup; + private Context mContext; + private Preference mPreference; + private ConnectedDeviceGroupController mConnectedDeviceGroupController; + private Lifecycle mLifecycle; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + mPreference = new Preference(mContext); + mLifecycle = new Lifecycle(() -> mLifecycle); + mPreferenceGroup = spy(new PreferenceScreen(mContext, null)); + doReturn(mPreferenceManager).when(mPreferenceGroup).getPreferenceManager(); + doReturn(mContext).when(mDashboardFragment).getContext(); + + mConnectedDeviceGroupController = new ConnectedDeviceGroupController(mDashboardFragment, + mLifecycle, mConnectedBluetoothDeviceUpdater); + mConnectedDeviceGroupController.mPreferenceGroup = mPreferenceGroup; + } + + @Test + public void testOnDeviceAdded_firstAdd_becomeVisible() { + mConnectedDeviceGroupController.onDeviceAdded(mPreference); + + assertThat(mPreferenceGroup.isVisible()).isTrue(); + } + + @Test + public void testOnDeviceRemoved_lastRemove_becomeInvisible() { + mPreferenceGroup.addPreference(mPreference); + + mConnectedDeviceGroupController.onDeviceRemoved(mPreference); + + assertThat(mPreferenceGroup.isVisible()).isFalse(); + } + + @Test + public void testOnDeviceRemoved_notLastRemove_stillVisible() { + mPreferenceGroup.setVisible(true); + mPreferenceGroup.addPreference(mPreference); + mPreferenceGroup.addPreference(new Preference(mContext)); + + mConnectedDeviceGroupController.onDeviceRemoved(mPreference); + + assertThat(mPreferenceGroup.isVisible()).isTrue(); + } + + @Test + public void testDisplayPreference_becomeInvisible() { + doReturn(mPreferenceGroup).when(mPreferenceScreen).findPreference(anyString()); + + mConnectedDeviceGroupController.displayPreference(mPreferenceScreen); + + assertThat(mPreferenceGroup.isVisible()).isFalse(); + } + + @Test + public void testLifecycle() { + // register the callback in onStart() + mLifecycle.handleLifecycleEvent(android.arch.lifecycle.Lifecycle.Event.ON_START); + verify(mConnectedBluetoothDeviceUpdater).registerCallback(); + + // unregister the callback in onStop() + mLifecycle.handleLifecycleEvent(android.arch.lifecycle.Lifecycle.Event.ON_STOP); + verify(mConnectedBluetoothDeviceUpdater).unregisterCallback(); + } +}