blob: 8488278d5835fbf14c66bc4181b9e04596dc9110 [file] [log] [blame]
/*
* Copyright (C) 2015 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.tv.settings.accessories;
import android.app.Fragment;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattService;
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 androidx.annotation.DrawableRes;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.leanback.app.GuidedStepFragment;
import androidx.leanback.widget.GuidanceStylist;
import androidx.leanback.widget.GuidedAction;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.logging.nano.MetricsProto;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.tv.settings.R;
import com.android.tv.settings.SettingsPreferenceFragment;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
/**
* The screen in TV settings that let's users rename or unpair a bluetooth device.
*/
public class BluetoothAccessoryFragment extends SettingsPreferenceFragment {
private static final boolean DEBUG = false;
private static final String TAG = "BluetoothAccessoryFrag";
private static final UUID GATT_BATTERY_SERVICE_UUID =
UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb");
private static final UUID GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID =
UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb");
private static final String KEY_CHANGE_NAME = "changeName";
private static final String KEY_UNPAIR = "unpair";
private static final String KEY_BATTERY = "battery";
private static final String SAVE_STATE_UNPAIRING = "BluetoothAccessoryActivity.unpairing";
private static final int UNPAIR_TIMEOUT = 5000;
private static final String ARG_DEVICE = "device";
private static final String ARG_ACCESSORY_ADDRESS = "accessory_address";
private static final String ARG_ACCESSORY_NAME = "accessory_name";
private static final String ARG_ACCESSORY_ICON_ID = "accessory_icon_res";
private BluetoothDevice mDevice;
private BluetoothGatt mDeviceGatt;
private String mDeviceAddress;
private String mDeviceName;
private @DrawableRes int mDeviceImgId;
private boolean mUnpairing;
private Preference mChangeNamePref;
private Preference mUnpairPref;
private Preference mBatteryPref;
private final Handler mHandler = new Handler();
private Runnable mBailoutRunnable = new Runnable() {
@Override
public void run() {
if (isResumed() && !getFragmentManager().popBackStackImmediate()) {
getActivity().onBackPressed();
}
}
};
// Broadcast Receiver for Bluetooth related events
private BroadcastReceiver mBroadcastReceiver;
public static BluetoothAccessoryFragment newInstance(String deviceAddress, String deviceName,
int deviceImgId) {
final Bundle b = new Bundle(3);
prepareArgs(b, deviceAddress, deviceName, deviceImgId);
final BluetoothAccessoryFragment f = new BluetoothAccessoryFragment();
f.setArguments(b);
return f;
}
public static void prepareArgs(Bundle b, String deviceAddress, String deviceName,
int deviceImgId) {
b.putString(ARG_ACCESSORY_ADDRESS, deviceAddress);
b.putString(ARG_ACCESSORY_NAME, deviceName);
b.putInt(ARG_ACCESSORY_ICON_ID, deviceImgId);
}
@Override
public void onCreate(Bundle savedInstanceState) {
Bundle bundle = getArguments();
if (bundle != null) {
mDeviceAddress = bundle.getString(ARG_ACCESSORY_ADDRESS);
mDeviceName = bundle.getString(ARG_ACCESSORY_NAME);
mDeviceImgId = bundle.getInt(ARG_ACCESSORY_ICON_ID);
} else {
mDeviceName = getString(R.string.accessory_options);
mDeviceImgId = R.drawable.ic_qs_bluetooth_not_connected;
}
mUnpairing = savedInstanceState != null
&& savedInstanceState.getBoolean(SAVE_STATE_UNPAIRING);
BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
if (btAdapter != null) {
final Set<BluetoothDevice> bondedDevices = btAdapter.getBondedDevices();
if (bondedDevices != null) {
for (BluetoothDevice device : bondedDevices) {
if (mDeviceAddress.equals(device.getAddress())) {
mDevice = device;
break;
}
}
}
}
if (mDevice == null) {
navigateBack();
}
super.onCreate(savedInstanceState);
}
@Override
public void onStart() {
super.onStart();
if (mDevice != null &&
(mDevice.getType() == BluetoothDevice.DEVICE_TYPE_LE ||
mDevice.getType() == BluetoothDevice.DEVICE_TYPE_DUAL)) {
// Only LE devices support GATT
mDeviceGatt = mDevice.connectGatt(getActivity(), true, new GattBatteryCallbacks());
}
// Set a broadcast receiver to let us know when the device has been removed
final IntentFilter adapterIntentFilter = new IntentFilter();
adapterIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
mBroadcastReceiver = new UnpairReceiver(this, mDevice);
getActivity().registerReceiver(mBroadcastReceiver, adapterIntentFilter);
if (mDevice != null && mDevice.getBondState() == BluetoothDevice.BOND_NONE) {
navigateBack();
}
}
@Override
public void onPause() {
super.onPause();
mHandler.removeCallbacks(mBailoutRunnable);
}
@Override
public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
savedInstanceState.putBoolean(SAVE_STATE_UNPAIRING, mUnpairing);
}
@Override
public void onStop() {
super.onStop();
if (mDeviceGatt != null) {
mDeviceGatt.close();
}
getActivity().unregisterReceiver(mBroadcastReceiver);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.bluetooth_accessory, null);
final PreferenceScreen screen = getPreferenceScreen();
screen.setTitle(mDeviceName);
mChangeNamePref = findPreference(KEY_CHANGE_NAME);
ChangeNameFragment.prepareArgs(mChangeNamePref.getExtras(), mDeviceName, mDeviceImgId);
mUnpairPref = findPreference(KEY_UNPAIR);
updatePrefsForUnpairing();
UnpairConfirmFragment.prepareArgs(
mUnpairPref.getExtras(), mDevice, mDeviceName, mDeviceImgId);
mBatteryPref = findPreference(KEY_BATTERY);
mBatteryPref.setVisible(false);
}
public void setUnpairing(boolean unpairing) {
mUnpairing = unpairing;
updatePrefsForUnpairing();
}
private void updatePrefsForUnpairing() {
if (mUnpairing) {
mUnpairPref.setTitle(R.string.accessory_unpairing);
mUnpairPref.setEnabled(false);
mChangeNamePref.setEnabled(false);
} else {
mUnpairPref.setTitle(R.string.accessory_unpair);
mUnpairPref.setEnabled(true);
mChangeNamePref.setEnabled(true);
}
}
private void navigateBack() {
// need to post this to avoid recursing in the fragment manager.
mHandler.removeCallbacks(mBailoutRunnable);
mHandler.post(mBailoutRunnable);
}
private void renameDevice(String deviceName) {
mDeviceName = deviceName;
if (mDevice != null) {
mDevice.setAlias(deviceName);
getPreferenceScreen().setTitle(deviceName);
setTitle(deviceName);
ChangeNameFragment.prepareArgs(mChangeNamePref.getExtras(), mDeviceName, mDeviceImgId);
UnpairConfirmFragment.prepareArgs(
mUnpairPref.getExtras(), mDevice, mDeviceName, mDeviceImgId);
}
}
private class GattBatteryCallbacks extends BluetoothGattCallback {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (DEBUG) {
Log.d(TAG, "Connection status:" + status + " state:" + newState);
}
if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) {
gatt.discoverServices();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status != BluetoothGatt.GATT_SUCCESS) {
if (DEBUG) {
Log.e(TAG, "Service discovery failure on " + gatt);
}
return;
}
final BluetoothGattService battService = gatt.getService(GATT_BATTERY_SERVICE_UUID);
if (battService == null) {
if (DEBUG) {
Log.d(TAG, "No battery service");
}
return;
}
final BluetoothGattCharacteristic battLevel =
battService.getCharacteristic(GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID);
if (battLevel == null) {
if (DEBUG) {
Log.d(TAG, "No battery level");
}
return;
}
gatt.readCharacteristic(battLevel);
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic, int status) {
if (status != BluetoothGatt.GATT_SUCCESS) {
if (DEBUG) {
Log.e(TAG, "Read characteristic failure on " + gatt + " " + characteristic);
}
return;
}
if (GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID.equals(characteristic.getUuid())) {
final int batteryLevel =
characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0);
mHandler.post(new Runnable() {
@Override
public void run() {
if (mBatteryPref != null && !mUnpairing) {
mBatteryPref.setTitle(getString(R.string.accessory_battery,
batteryLevel));
mBatteryPref.setVisible(true);
}
}
});
}
}
}
/**
* Fragment for changing the name of a bluetooth accessory
*/
@Keep
public static class ChangeNameFragment extends GuidedStepFragment {
private final MetricsFeatureProvider mMetricsFeatureProvider = new MetricsFeatureProvider();
public static void prepareArgs(@NonNull Bundle args, String deviceName,
@DrawableRes int deviceImgId) {
args.putString(ARG_ACCESSORY_NAME, deviceName);
args.putInt(ARG_ACCESSORY_ICON_ID, deviceImgId);
}
@Override
public void onStart() {
super.onStart();
mMetricsFeatureProvider.action(getContext(),
MetricsProto.MetricsEvent.ACTION_BLUETOOTH_RENAME);
}
@NonNull
@Override
public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
return new GuidanceStylist.Guidance(
getString(R.string.accessory_change_name_title),
null,
getArguments().getString(ARG_ACCESSORY_NAME),
getContext().getDrawable(getArguments().getInt(ARG_ACCESSORY_ICON_ID,
R.drawable.ic_qs_bluetooth_not_connected))
);
}
@Override
public void onCreateActions(@NonNull List<GuidedAction> actions,
Bundle savedInstanceState) {
final Context context = getContext();
actions.add(new GuidedAction.Builder(context)
.title(getArguments().getString(ARG_ACCESSORY_NAME))
.editable(true)
.build());
}
@Override
public long onGuidedActionEditedAndProceed(GuidedAction action) {
if (!TextUtils.equals(action.getTitle(),
getArguments().getString(ARG_ACCESSORY_NAME))
&& TextUtils.isGraphic(action.getTitle())) {
final BluetoothAccessoryFragment fragment =
(BluetoothAccessoryFragment) getTargetFragment();
fragment.renameDevice(action.getTitle().toString());
getFragmentManager().popBackStack();
}
return GuidedAction.ACTION_ID_NEXT;
}
}
private static class UnpairReceiver extends BroadcastReceiver {
private final Fragment mFragment;
private final BluetoothDevice mDevice;
public UnpairReceiver(Fragment fragment, BluetoothDevice device) {
mFragment = fragment;
mDevice = device;
}
@Override
public void onReceive(Context context, Intent intent) {
final BluetoothDevice device = intent
.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
BluetoothDevice.BOND_NONE);
if (bondState == BluetoothDevice.BOND_NONE && Objects.equals(mDevice, device)) {
// Device was removed, bail out of the fragment
if (mFragment instanceof BluetoothAccessoryFragment) {
((BluetoothAccessoryFragment) mFragment).navigateBack();
} else if (mFragment instanceof UnpairConfirmFragment) {
((UnpairConfirmFragment) mFragment).navigateBack();
} else {
throw new IllegalStateException(
"UnpairReceiver attached to wrong fragment class");
}
}
}
}
public static class UnpairConfirmFragment extends GuidedStepFragment {
private BluetoothDevice mDevice;
private BroadcastReceiver mBroadcastReceiver;
private final Handler mHandler = new Handler();
private final MetricsFeatureProvider mMetricsFeatureProvider =
new MetricsFeatureProvider();
private Runnable mBailoutRunnable = new Runnable() {
@Override
public void run() {
if (isResumed() && !getFragmentManager().popBackStackImmediate()) {
getActivity().onBackPressed();
}
}
};
private final Runnable mTimeoutRunnable = new Runnable() {
@Override
public void run() {
navigateBack();
}
};
public static void prepareArgs(@NonNull Bundle args, BluetoothDevice device,
String deviceName, @DrawableRes int deviceImgId) {
args.putParcelable(ARG_DEVICE, device);
args.putString(ARG_ACCESSORY_NAME, deviceName);
args.putInt(ARG_ACCESSORY_ICON_ID, deviceImgId);
}
@Override
public void onCreate(Bundle savedInstanceState) {
mDevice = getArguments().getParcelable(ARG_DEVICE);
super.onCreate(savedInstanceState);
}
@Override
public void onStart() {
super.onStart();
if (mDevice.getBondState() == BluetoothDevice.BOND_NONE) {
navigateBack();
}
final IntentFilter adapterIntentFilter = new IntentFilter();
adapterIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
mBroadcastReceiver = new UnpairReceiver(this, mDevice);
getActivity().registerReceiver(mBroadcastReceiver, adapterIntentFilter);
mMetricsFeatureProvider.action(getContext(),
MetricsProto.MetricsEvent.DIALOG_BLUETOOTH_PAIRED_DEVICE_FORGET);
}
@Override
public void onStop() {
super.onStop();
getActivity().unregisterReceiver(mBroadcastReceiver);
}
@Override
public void onDestroy() {
super.onDestroy();
mHandler.removeCallbacks(mTimeoutRunnable);
mHandler.removeCallbacks(mBailoutRunnable);
}
@NonNull
@Override
public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) {
return new GuidanceStylist.Guidance(
getString(R.string.accessory_unpair),
null,
getArguments().getString(ARG_ACCESSORY_NAME),
getContext().getDrawable(getArguments().getInt(ARG_ACCESSORY_ICON_ID,
R.drawable.ic_qs_bluetooth_not_connected))
);
}
@Override
public void onCreateActions(@NonNull List<GuidedAction> actions,
Bundle savedInstanceState) {
final Context context = getContext();
actions.add(new GuidedAction.Builder(context)
.clickAction(GuidedAction.ACTION_ID_OK).build());
actions.add(new GuidedAction.Builder(context)
.clickAction(GuidedAction.ACTION_ID_CANCEL).build());
}
@Override
public void onGuidedActionClicked(GuidedAction action) {
if (action.getId() == GuidedAction.ACTION_ID_OK) {
unpairDevice();
} else if (action.getId() == GuidedAction.ACTION_ID_CANCEL) {
getFragmentManager().popBackStack();
} else {
super.onGuidedActionClicked(action);
}
}
private void navigateBack() {
// need to post this to avoid recursing in the fragment manager.
mHandler.removeCallbacks(mBailoutRunnable);
mHandler.post(mBailoutRunnable);
}
private void unpairDevice() {
if (mDevice != null) {
int state = mDevice.getBondState();
if (state == BluetoothDevice.BOND_BONDING) {
mDevice.cancelBondProcess();
}
if (state != BluetoothDevice.BOND_NONE) {
((BluetoothAccessoryFragment) getTargetFragment()).setUnpairing(true);
// Set a timeout, just in case we don't receive the unpair notification we
// use to finish the activity
mHandler.postDelayed(mTimeoutRunnable, UNPAIR_TIMEOUT);
final boolean successful = mDevice.removeBond();
if (successful) {
if (DEBUG) {
Log.d(TAG, "Bluetooth device successfully unpaired.");
}
} else {
Log.e(TAG, "Failed to unpair Bluetooth Device: " + mDevice.getName());
}
}
} else {
Log.e(TAG, "Bluetooth device not found. Address = " + mDevice.getAddress());
}
}
}
@Override
public int getMetricsCategory() {
return MetricsProto.MetricsEvent.DIALOG_BLUETOOTH_PAIRED_DEVICE_PROFILE;
}
}