From 2066ca63ba8f0765ee30e1be14552866da35d8ac Mon Sep 17 00:00:00 2001 From: Anthony Chen Date: Fri, 27 Jan 2017 11:00:04 -0800 Subject: Bluetooth configuration for SetupWizard. This bluetooth activity will ask the user to pair a bluetooth device. Upon completion of a successful pairing, the screen will move onto the next action. This means during setup, only a single bluetooth item will be set up. Test: manually tested Bug: 34378933 Change-Id: I4469c274cf0ba063a871b8772d33496a99de4d5b --- SetupWizard/Android.mk | 45 +++ SetupWizard/AndroidManifest.xml | 47 +++ SetupWizard/res/drawable/ic_bluetooth.xml | 26 ++ .../res/drawable/ic_bluetooth_connected.xml | 26 ++ SetupWizard/res/drawable/ic_bluetooth_item.xml | 26 ++ SetupWizard/res/drawable/ic_bluetooth_scanning.xml | 26 ++ SetupWizard/res/drawable/ic_computer.xml | 25 ++ SetupWizard/res/drawable/ic_headset.xml | 25 ++ SetupWizard/res/drawable/ic_refresh.xml | 26 ++ SetupWizard/res/drawable/ic_skip.xml | 26 ++ SetupWizard/res/drawable/ic_smartphone.xml | 25 ++ SetupWizard/res/drawable/ic_watch.xml | 25 ++ SetupWizard/res/layout/bluetooth_activity.xml | 26 ++ SetupWizard/res/layout/items_greyed_out.xml | 70 ++++ SetupWizard/res/values/dimens.xml | 19 + SetupWizard/res/values/strings.xml | 47 +++ SetupWizard/res/xml/items_bluetooth.xml | 44 +++ .../res/xml/wizard_script_setup_as_new_flow.xml | 191 +++++++++ SetupWizard/res/xml/wizard_script_user.xml | 163 ++++++++ .../setupwizard/bluetooth/BluetoothActivity.java | 427 +++++++++++++++++++++ .../bluetooth/BluetoothDeviceHierarchy.java | 277 +++++++++++++ 21 files changed, 1612 insertions(+) create mode 100644 SetupWizard/Android.mk create mode 100644 SetupWizard/AndroidManifest.xml create mode 100644 SetupWizard/res/drawable/ic_bluetooth.xml create mode 100644 SetupWizard/res/drawable/ic_bluetooth_connected.xml create mode 100644 SetupWizard/res/drawable/ic_bluetooth_item.xml create mode 100644 SetupWizard/res/drawable/ic_bluetooth_scanning.xml create mode 100644 SetupWizard/res/drawable/ic_computer.xml create mode 100644 SetupWizard/res/drawable/ic_headset.xml create mode 100644 SetupWizard/res/drawable/ic_refresh.xml create mode 100644 SetupWizard/res/drawable/ic_skip.xml create mode 100644 SetupWizard/res/drawable/ic_smartphone.xml create mode 100644 SetupWizard/res/drawable/ic_watch.xml create mode 100644 SetupWizard/res/layout/bluetooth_activity.xml create mode 100644 SetupWizard/res/layout/items_greyed_out.xml create mode 100644 SetupWizard/res/values/dimens.xml create mode 100644 SetupWizard/res/values/strings.xml create mode 100644 SetupWizard/res/xml/items_bluetooth.xml create mode 100644 SetupWizard/res/xml/wizard_script_setup_as_new_flow.xml create mode 100644 SetupWizard/res/xml/wizard_script_user.xml create mode 100644 SetupWizard/src/com/android/car/setupwizard/bluetooth/BluetoothActivity.java create mode 100644 SetupWizard/src/com/android/car/setupwizard/bluetooth/BluetoothDeviceHierarchy.java (limited to 'SetupWizard') diff --git a/SetupWizard/Android.mk b/SetupWizard/Android.mk new file mode 100644 index 0000000..3a151f2 --- /dev/null +++ b/SetupWizard/Android.mk @@ -0,0 +1,45 @@ +# +# 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. +# + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res + +LOCAL_PACKAGE_NAME := CarSetupWizard + +LOCAL_CERTIFICATE := platform + +LOCAL_MODULE_TAGS := optional + +LOCAL_PRIVILEGED_MODULE := true + +# For setup wizard common lib +LOCAL_STATIC_JAVA_LIBRARIES += android-setup-wizard-common +LOCAL_AAPT_FLAGS += --auto-add-overlay --extra-packages com.google.android.setupwizard.common +LOCAL_RESOURCE_DIR += vendor/google/apps/SetupWizard/libs/base/res + +LOCAL_PROGUARD_ENABLED := disabled + +LOCAL_DEX_PREOPT := false + +include frameworks/opt/setupwizard/library/common-gingerbread.mk +include frameworks/base/packages/SettingsLib/common.mk + +include $(BUILD_PACKAGE) diff --git a/SetupWizard/AndroidManifest.xml b/SetupWizard/AndroidManifest.xml new file mode 100644 index 0000000..0e7d0ca --- /dev/null +++ b/SetupWizard/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + diff --git a/SetupWizard/res/drawable/ic_bluetooth.xml b/SetupWizard/res/drawable/ic_bluetooth.xml new file mode 100644 index 0000000..3e96002 --- /dev/null +++ b/SetupWizard/res/drawable/ic_bluetooth.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/SetupWizard/res/drawable/ic_bluetooth_connected.xml b/SetupWizard/res/drawable/ic_bluetooth_connected.xml new file mode 100644 index 0000000..7e08c89 --- /dev/null +++ b/SetupWizard/res/drawable/ic_bluetooth_connected.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/SetupWizard/res/drawable/ic_bluetooth_item.xml b/SetupWizard/res/drawable/ic_bluetooth_item.xml new file mode 100644 index 0000000..a1841c0 --- /dev/null +++ b/SetupWizard/res/drawable/ic_bluetooth_item.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/SetupWizard/res/drawable/ic_bluetooth_scanning.xml b/SetupWizard/res/drawable/ic_bluetooth_scanning.xml new file mode 100644 index 0000000..0a8d064 --- /dev/null +++ b/SetupWizard/res/drawable/ic_bluetooth_scanning.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/SetupWizard/res/drawable/ic_computer.xml b/SetupWizard/res/drawable/ic_computer.xml new file mode 100644 index 0000000..e7020ee --- /dev/null +++ b/SetupWizard/res/drawable/ic_computer.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/SetupWizard/res/drawable/ic_headset.xml b/SetupWizard/res/drawable/ic_headset.xml new file mode 100644 index 0000000..ef805c1 --- /dev/null +++ b/SetupWizard/res/drawable/ic_headset.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/SetupWizard/res/drawable/ic_refresh.xml b/SetupWizard/res/drawable/ic_refresh.xml new file mode 100644 index 0000000..5c39440 --- /dev/null +++ b/SetupWizard/res/drawable/ic_refresh.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/SetupWizard/res/drawable/ic_skip.xml b/SetupWizard/res/drawable/ic_skip.xml new file mode 100644 index 0000000..8707990 --- /dev/null +++ b/SetupWizard/res/drawable/ic_skip.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/SetupWizard/res/drawable/ic_smartphone.xml b/SetupWizard/res/drawable/ic_smartphone.xml new file mode 100644 index 0000000..050d73c --- /dev/null +++ b/SetupWizard/res/drawable/ic_smartphone.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/SetupWizard/res/drawable/ic_watch.xml b/SetupWizard/res/drawable/ic_watch.xml new file mode 100644 index 0000000..1346e5b --- /dev/null +++ b/SetupWizard/res/drawable/ic_watch.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/SetupWizard/res/layout/bluetooth_activity.xml b/SetupWizard/res/layout/bluetooth_activity.xml new file mode 100644 index 0000000..9e32b4d --- /dev/null +++ b/SetupWizard/res/layout/bluetooth_activity.xml @@ -0,0 +1,26 @@ + + + diff --git a/SetupWizard/res/layout/items_greyed_out.xml b/SetupWizard/res/layout/items_greyed_out.xml new file mode 100644 index 0000000..0c190c5 --- /dev/null +++ b/SetupWizard/res/layout/items_greyed_out.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/SetupWizard/res/values/dimens.xml b/SetupWizard/res/values/dimens.xml new file mode 100644 index 0000000..6ce560c --- /dev/null +++ b/SetupWizard/res/values/dimens.xml @@ -0,0 +1,19 @@ + + + + 32dp + diff --git a/SetupWizard/res/values/strings.xml b/SetupWizard/res/values/strings.xml new file mode 100644 index 0000000..8b55140 --- /dev/null +++ b/SetupWizard/res/values/strings.xml @@ -0,0 +1,47 @@ + + + + + Setup Wizard + + + Pair a device + + + Searching for Bluetooth devices\u2026 + + + Don\u2019t pair to any bluetooth device + + + Refresh + + + Pairing\u2026 + + + Unpairing\u2026 + + + Cancelling\u2026 + + + Connected + + + Select a device from the list to pair to it via bluetooth. + diff --git a/SetupWizard/res/xml/items_bluetooth.xml b/SetupWizard/res/xml/items_bluetooth.xml new file mode 100644 index 0000000..ed538c8 --- /dev/null +++ b/SetupWizard/res/xml/items_bluetooth.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + diff --git a/SetupWizard/res/xml/wizard_script_setup_as_new_flow.xml b/SetupWizard/res/xml/wizard_script_setup_as_new_flow.xml new file mode 100644 index 0000000..c55bebc --- /dev/null +++ b/SetupWizard/res/xml/wizard_script_setup_as_new_flow.xml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SetupWizard/res/xml/wizard_script_user.xml b/SetupWizard/res/xml/wizard_script_user.xml new file mode 100644 index 0000000..10c91a7 --- /dev/null +++ b/SetupWizard/res/xml/wizard_script_user.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SetupWizard/src/com/android/car/setupwizard/bluetooth/BluetoothActivity.java b/SetupWizard/src/com/android/car/setupwizard/bluetooth/BluetoothActivity.java new file mode 100644 index 0000000..734e1ee --- /dev/null +++ b/SetupWizard/src/com/android/car/setupwizard/bluetooth/BluetoothActivity.java @@ -0,0 +1,427 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * 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.car.setupwizard.bluetooth; + +import static com.android.setupwizardlib.util.ResultCodes.RESULT_SKIP; + +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.View; + +import com.android.car.setupwizard.R; +import com.android.car.setupwizard.bluetooth.BluetoothDeviceHierarchy.BluetoothItem; + +import com.android.setupwizardlib.GlifRecyclerLayout; +import com.android.setupwizardlib.items.IItem; +import com.android.setupwizardlib.items.Item; +import com.android.setupwizardlib.items.ItemGroup; +import com.android.setupwizardlib.items.RecyclerItemAdapter; +import com.android.setupwizardlib.util.ResultCodes; +import com.android.setupwizardlib.util.WizardManagerHelper; + +/** + * An Activity that presents the option for the user to pair the current device to a nearby + * bluetooth device. This screen will list the devices in the order that they are discovered + * as well as an option to not pair at all. + */ +public class BluetoothActivity extends Activity + implements RecyclerItemAdapter.OnItemSelectedListener { + private static final String TAG = "BluetoothActivity"; + + /** + * This value is copied from {@code com.google.android.setupwizard.BaseActivity}. Wizard + * Manager does not actually return an activity result, but if we invoke Wizard Manager without + * requesting a result, the framework will choose not to issue a call to onActivityResult with + * RESULT_CANCELED when navigating backward. + */ + private static final int REQUEST_CODE_NEXT = 10000; + + private static final int BLUETOOTH_SCAN_RETRY_DELAY = 1000; + private static final int MAX_BLUETOOTH_SCAN_RETRIES = 3; + + private final Handler mHandler = new Handler(); + private int mScanRetryCount; + + private BluetoothScanReceiver mScanReceiver; + private BluetoothAdapterReceiver mAdapterReceiver; + private BluetoothAdapter mAdapter; + private BluetoothDeviceHierarchy mBluetoothDeviceHierarchy; + + private GlifRecyclerLayout mLayout; + private Item mScanningIndicator; + private Item mRescanIndicator; + + /** + * The current {@link BluetoothDevice} that is being paired to. + */ + private BluetoothDevice mCurrentBondingDevice; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (mAdapter == null) { + Log.w(TAG, "No bluetooth adapter found on the device. Skipping to next action."); + nextAction(RESULT_SKIP); + return; + } + + setContentView(R.layout.bluetooth_activity); + + mLayout = (GlifRecyclerLayout) findViewById(R.id.setup_wizard_layout); + + RecyclerItemAdapter adapter = (RecyclerItemAdapter) mLayout.getAdapter(); + adapter.setOnItemSelectedListener(this); + + ItemGroup hierarchy = (ItemGroup) adapter.getRootItemHierarchy(); + mBluetoothDeviceHierarchy = + (BluetoothDeviceHierarchy) hierarchy.findItemById(R.id.bluetooth_device_list); + mScanningIndicator = (Item) hierarchy.findItemById(R.id.bluetooth_scanning); + mRescanIndicator = (Item) hierarchy.findItemById(R.id.bluetooth_rescan); + + Item descriptionItem = (Item) hierarchy.findItemById(R.id.bluetooth_description); + descriptionItem.setTitle(getText(R.string.bluetooth_description)); + + // Assume that a search will be started, so display the progress bar to let the user + // know that something is going on. + mLayout.setProgressBarShown(true); + + if (mAdapter.isEnabled()) { + setUpAndStartScan(); + } else { + mAdapterReceiver = new BluetoothAdapterReceiver(); + maybeRegisterAdapterReceiver(); + mAdapter.enable(); + } + } + + @Override + protected void onStart() { + super.onStart(); + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "onStart()"); + } + + if (mAdapter == null) { + Log.w(TAG, "No bluetooth adapter found on the device. Skipping to next action."); + nextAction(RESULT_SKIP); + return; + } + + maybeRegisterAdapterReceiver(); + registerScanReceiver(); + } + + @Override + protected void onStop() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "onStop()"); + } + + stopScanning(); + + if (mScanReceiver != null) { + unregisterReceiver(mScanReceiver); + } + + if (mAdapterReceiver != null) { + unregisterReceiver(mAdapterReceiver); + } + + super.onStop(); + } + + /** + * Sets up an Intent filter to listen for bluetooth state changes and initiates a scan for + * nearby bluetooth devices. + */ + private void setUpAndStartScan() { + mBluetoothDeviceHierarchy.clearAllDevices(); + registerScanReceiver(); + startScanning(); + } + + /** + * Registers a receiver to be listen on changes to the {@link BluetoothAdapter}. This method + * will only register the receiver if {@link #mAdapterReceiver} is not {@code null}. + */ + private void maybeRegisterAdapterReceiver() { + if (mAdapterReceiver == null) { + return; + } + + IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); + registerReceiver(mAdapterReceiver, filter); + } + + /** + * Registers an Intent filter to listen for the results of a bluetooth discovery scan as well as + * changes to individual bluetooth devices. + */ + private void registerScanReceiver() { + if (mScanReceiver == null) { + mScanReceiver = new BluetoothScanReceiver(); + } + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); + intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); + intentFilter.addAction(BluetoothDevice.ACTION_FOUND); + intentFilter.addAction(BluetoothDevice.ACTION_NAME_CHANGED); + intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + registerReceiver(mScanReceiver, intentFilter); + } + + /** + * Start a scan for nearby bluetooth devices. If the call to + * {@link BluetoothAdapter#startDiscovery()} fails, then this method will retry the call after + * an exponential backoff period based on {@link #BLUETOOTH_SCAN_RETRY_DELAY}. + * + *

If there is already a bluetooth scan in progress when this function is called, then this + * function will do nothing. + */ + private void startScanning() { + if (mAdapter.isDiscovering()) { + return; + } + + boolean success = mAdapter.startDiscovery(); + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "startDiscovery() success: " + success); + } + + // If a scan fails, attempt to try again up to MAX_BLUETOOTH_SCAN_RETRIES tries. + if (success) { + mScanRetryCount = 0; + } else if (mScanRetryCount >= MAX_BLUETOOTH_SCAN_RETRIES) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Reached max retries to initiate a bluetooth scan. Moving onto next " + + "action"); + } + + nextAction(RESULT_SKIP); + } else { + mHandler.postDelayed(this::startScanning, + BLUETOOTH_SCAN_RETRY_DELAY * ++mScanRetryCount); + } + } + + /** + * Stops any scan in that is currently in progress for nearby bluetooth devices. + */ + private void stopScanning() { + if (mAdapter != null && mAdapter.isDiscovering()) { + mAdapter.cancelDiscovery(); + } + + mScanRetryCount = 0; + } + + @Override + public void onItemSelected(IItem item) { + if (item instanceof BluetoothItem) { + pairOrUnpairDevice((BluetoothItem) item); + return; + } + + if (!(item instanceof Item)) { + return; + } + + switch (((Item) item).getId()) { + case R.id.bluetooth_dont_connect: + nextAction(RESULT_SKIP); + break; + + case R.id.bluetooth_rescan: + stopScanning(); + startScanning(); + break; + + default: + Log.w(TAG, "Unknown item clicked: " + item); + } + } + + /** + * Starts a pairing or unpairing session with the given device based on its current bonded + * state. For example, if the current item is already paired, it is unpaired and vice versa. + */ + private void pairOrUnpairDevice(BluetoothItem item) { + // Pairing is unreliable while scanning, so cancel discovery. + stopScanning(); + + BluetoothDevice device = item.getBluetoothDevice(); + + boolean success; + switch (device.getBondState()) { + case BluetoothDevice.BOND_BONDED: + mCurrentBondingDevice = null; + success = device.removeBond(); + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "removeBond() to device (" + device + ") successful: " + success); + } + + // Immediately update the UI so that the user has feedback on their actions. + item.updateConnectionState(this /* context */, + BluetoothItem.CONNECTION_STATE_DISCONNECTING); + break; + + case BluetoothDevice.BOND_BONDING: + mCurrentBondingDevice = null; + success = device.cancelBondProcess(); + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "cancelBondProcess() to device (" + device + ") successful: " + + success); + } + + // Immediately update the UI so that the user has feedback on their actions. + item.updateConnectionState(this /* context */, + BluetoothItem.CONNECTION_STATE_CANCELLING); + break; + + case BluetoothDevice.BOND_NONE: + mCurrentBondingDevice = device; + success = device.createBond(); + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "createBond() to device (" + device + ") successful: " + success); + } + + // Immediately update the UI so that the user has feedback on their actions. + item.updateConnectionState(this /* context */, + BluetoothItem.CONNECTION_STATE_CONNECTING); + + default: + Log.w(TAG, "Encountered unknown bond state: " + device.getBondState()); + } + } + + private void nextAction(int resultCode) { + setResult(resultCode); + Intent nextIntent = WizardManagerHelper.getNextIntent(getIntent(), resultCode); + startActivityForResult(nextIntent, REQUEST_CODE_NEXT); + } + + /** + * A {@link BroadReceiver} that listens for when the bluetooth adapter has been turned on. + */ + private class BluetoothAdapterReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + if (action != null && action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, + BluetoothAdapter.ERROR); + + if (state == BluetoothAdapter.STATE_ON) { + setUpAndStartScan(); + } + } + } + } + + /** + * Handles bluetooth scan responses and other indicators. + **/ + private class BluetoothScanReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + if (action == null) { + return; + } + + BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Received device: " + device); + } + + switch (action) { + case BluetoothDevice.ACTION_FOUND: + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Bluetooth device found"); + } + + mLayout.setProgressBarShown(false); + mScanningIndicator.setVisible(false); + mRescanIndicator.setVisible(true); + mBluetoothDeviceHierarchy.addOrUpdateDevice(context, device); + break; + + case BluetoothAdapter.ACTION_DISCOVERY_STARTED: + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Bluetooth discovery started"); + } + + mLayout.setProgressBarShown(true); + mScanningIndicator.setVisible(true); + mRescanIndicator.setVisible(false); + mBluetoothDeviceHierarchy.clearAllDevices(); + break; + + case BluetoothAdapter.ACTION_DISCOVERY_FINISHED: + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Bluetooth discovery finished"); + } + break; + + case BluetoothDevice.ACTION_BOND_STATE_CHANGED: + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Bluetooth bond state changed"); + } + + mBluetoothDeviceHierarchy.addOrUpdateDevice(context, device); + + // When a bluetooth device has been paired, then move onto the next action so + // the user is not stuck on this screen for too long. + if (device.equals(mCurrentBondingDevice) + && device.getBondState() == BluetoothDevice.BOND_BONDED) { + nextAction(RESULT_OK); + } + break; + + case BluetoothDevice.ACTION_NAME_CHANGED: + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Bluetooth device name chaged"); + } + mBluetoothDeviceHierarchy.addOrUpdateDevice(context, device); + break; + + default: + Log.w(TAG, "Unknown action received: " + action); + } + } + } +} diff --git a/SetupWizard/src/com/android/car/setupwizard/bluetooth/BluetoothDeviceHierarchy.java b/SetupWizard/src/com/android/car/setupwizard/bluetooth/BluetoothDeviceHierarchy.java new file mode 100644 index 0000000..02f76bf --- /dev/null +++ b/SetupWizard/src/com/android/car/setupwizard/bluetooth/BluetoothDeviceHierarchy.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * 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.car.setupwizard.bluetooth; + +import android.bluetooth.BluetoothClass.Device; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.support.annotation.DrawableRes; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import com.android.car.setupwizard.R; + +import com.android.setupwizardlib.items.AbstractItemHierarchy; +import com.android.setupwizardlib.items.IItem; +import com.android.setupwizardlib.items.Item; +import com.android.setupwizardlib.items.ItemHierarchy; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * An item hierarchy that represents a list of Bluetooth devices. + */ +public class BluetoothDeviceHierarchy extends AbstractItemHierarchy { + private static final String TAG = "BtDeviceHierarchy"; + + /** + * A set of all discovered bluetooth devices. The key of this map is the device's MAC address. + */ + private final HashMap mItems = new HashMap<>(); + + /** + * A list of all discovered bluetooth devices' MAC addresses. This list is sorted in the order + * that the devices were discovered in. + */ + private final List mAddresses = new ArrayList<>(); + + public BluetoothDeviceHierarchy(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * Clears the current list of all bluetooth devices. + */ + public void clearAllDevices() { + mItems.clear(); + mAddresses.clear(); + notifyChanged(); + } + + /** + * Adds the given {@link BluetoothDevice} to be displayed. If the device has already been + * added before, its information is updated based on the given {@code BluetoothDevice}. + */ + public void addOrUpdateDevice(Context context, @Nullable BluetoothDevice device) { + if (device == null) { + return; + } + + String address = device.getAddress(); + BluetoothItem item; + + if (mItems.containsKey(address)) { + item = mItems.get(address); + } else { + // First time encountering this address, so keep track of it. + mAddresses.add(address); + + int id = View.generateViewId(); + if (id >= 0x00ffffff) { + // View.generateViewId returns an incrementing number from 1 to 0x00ffffff. + // Since we are generating view IDs without recycling, it is theoretically possible + // for the ID space to run out if the user encounters enough bluetooth devices. + // Just log if this happens. + Log.e(TAG, "Ran out of IDs to use for bluetooth item IDs"); + } + item = new BluetoothItem(id); + } + + item.update(context, device); + mItems.put(address, item); + + notifyChanged(); + } + + @Override + public ItemHierarchy findItemById(int id) { + if (id == getId()) { + return this; + } + + // Child items have generated hash code IDs. Don't try to find those. + return null; + } + + @Override + public int getCount() { + return mItems.size(); + } + + @Override + public IItem getItemAt(int position) { + return mItems.get(mAddresses.get(position)); + } + + /** + * A {@link Item} that is linked to a particular {@link BluetoothDevice} and is responsible + * for binding this information to a View to be displayed. + */ + public static class BluetoothItem extends Item { + private BluetoothDevice mDevice; + + /** + * Whether or not the icon for this particular BluetoothDevice has been updated to reflect + * the type of Bluetooth device this is. + */ + private boolean mIconUpdated; + + public static final int CONNECTION_STATE_DISCONNECTING = 0; + public static final int CONNECTION_STATE_CONNECTING = 1; + public static final int CONNECTION_STATE_CANCELLING = 2; + + @IntDef({ + CONNECTION_STATE_DISCONNECTING, + CONNECTION_STATE_CONNECTING, + CONNECTION_STATE_CANCELLING }) + public @interface ConnectionState {} + + public BluetoothItem(int id) { + setId(id); + } + + /** + * Immediately updates the connection state of the device that is represented by this + * {@link BluetoothItem}. This state is not necessarily tied to the bonded state that + * will be returned by the {@link BluetoothDevice} associated with this item. + */ + public void updateConnectionState(Context context, @ConnectionState int state) { + if (mDevice == null) { + return; + } + + switch (state) { + case CONNECTION_STATE_DISCONNECTING: + setSummary(context.getString(R.string.bluetooth_device_disconnecting)); + break; + + case CONNECTION_STATE_CONNECTING: + setSummary(context.getString(R.string.bluetooth_device_connecting)); + break; + + case CONNECTION_STATE_CANCELLING: + setSummary(context.getString(R.string.bluetooth_device_cancelling)); + break; + + default: + // Do nothing. + } + } + + /** + * Associate a {@link BluetoothDevice} with this {@link BluetoothItem}. + */ + public void update(Context context, BluetoothDevice device) { + mIconUpdated = false; + mDevice = device; + + String name = mDevice.getName(); + setTitle(TextUtils.isEmpty(name) ? mDevice.getAddress() : name); + + switch (mDevice.getBondState()) { + case BluetoothDevice.BOND_BONDED: + setSummary(context.getString(R.string.bluetooth_device_connected)); + break; + + case BluetoothDevice.BOND_BONDING: + setSummary(context.getString(R.string.bluetooth_device_connecting)); + break; + + default: + setSummary(null); + } + } + + /** + * Returns the {@link BluetoothDevice} set via {@link #update(Context, BluetoothDevice)}. + */ + public BluetoothDevice getBluetoothDevice() { + return mDevice; + } + + @Override + public void onBindView(View view) { + if (mIconUpdated && getIcon() != null) { + super.onBindView(view); + return; + } + + Context context = view.getContext(); + TypedArray a = context.obtainStyledAttributes( + new int[] { R.attr.suwListItemIconColor }); + + try { + ColorStateList bluetoothIconColor = a.getColorStateList(0); + Drawable bluetoothIcon = getDeviceIcon(context).mutate(); + bluetoothIcon.setTintList(bluetoothIconColor); + setIcon(bluetoothIcon); + } finally { + a.recycle(); + } + + mIconUpdated = true; + + super.onBindView(view); + } + + /** + * Returns an appropriate {@link Drawable} to use as the icon for the bluetooth device + * associated with this {@link BluetoothItem}. + */ + private Drawable getDeviceIcon(Context context) { + if (mDevice == null) { + return context.getDrawable(R.drawable.ic_bluetooth_item); + } + + @DrawableRes int deviceIcon; + switch (mDevice.getBluetoothClass().getDeviceClass()) { + case Device.AUDIO_VIDEO_HEADPHONES: + case Device.AUDIO_VIDEO_WEARABLE_HEADSET: + deviceIcon = R.drawable.ic_headset; + break; + + case Device.COMPUTER_DESKTOP: + case Device.COMPUTER_LAPTOP: + deviceIcon = R.drawable.ic_computer; + break; + + case Device.PHONE_SMART: + deviceIcon = R.drawable.ic_smartphone; + break; + + case Device.WEARABLE_WRIST_WATCH: + deviceIcon = R.drawable.ic_watch; + break; + + default: + deviceIcon = R.drawable.ic_bluetooth_item; + } + + return context.getDrawable(deviceIcon); + } + } +} -- cgit v1.2.3