There are two problems with the Bluetooth settings and pairing pages that are fixed by this CL: (1) We advertise on the page that the local device is visible to other devices, but that only lasts for the length of the default timeout (120 seconds) for the local adapter being in discoverable mode. (2) Both the BluetoothSettings and BluetoothPairingDetail fragments enter discoverable mode in their onStart handler and exit it in their onStop handler. Unfortunately when doing a fragment navigation the onStart and onStop events interleave in a non-intuitive manner. When you go from BluetoothSettings to BluetoothPairingDetail, we see the onStop event for BluetoothSettings *after* the onStart event for BluetoothPairingDetail, and similarly when going back from BluetoothSettings to BluetoothPairingDetail. What this means in practice is that if you go to the BluetoothSettings page, the device will be discoverable, but once you navigate to BluetoothPairingDetail or back again you won't be discoverable again until you go somewhere else or end the settings activity. This CL adds a new object called AlwaysDiscoverable which can be used to start and stop a mode of "always being discoverable". While started, it will listen for changes to the discoverable state, and return to discoverable mode. This fixes (1) by returning to discoverable mode whenever the normal timeout expires, and (2) similary by returning to discoverable mode when we accidentally exit it due to the onStop/onStart mismatch. A better fix for (2) would be to avoid the "glitch" of briefly exiting discoverable mode only to re-enter it, but the implementation of that is a little more complicated so that's being left as future work in order to keep this CL as small as possible. Bug: 64130265 Test: make RunSettingsRoboTests Change-Id: I559dd8187263ea6a0008be1a8abdfffac97cb87a
451 lines
17 KiB
Java
451 lines
17 KiB
Java
/*
|
|
* Copyright (C) 2011 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.app.Activity;
|
|
import android.app.Fragment;
|
|
import android.bluetooth.BluetoothAdapter;
|
|
import android.bluetooth.BluetoothDevice;
|
|
import android.content.ContentResolver;
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.os.Bundle;
|
|
import android.os.SystemProperties;
|
|
import android.provider.Settings;
|
|
import android.support.annotation.VisibleForTesting;
|
|
import android.support.v7.preference.Preference;
|
|
import android.support.v7.preference.PreferenceGroup;
|
|
import android.text.Spannable;
|
|
import android.text.style.TextAppearanceSpan;
|
|
import android.util.Log;
|
|
import android.view.View;
|
|
import android.widget.TextView;
|
|
|
|
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
|
import com.android.settings.LinkifyUtils;
|
|
import com.android.settings.R;
|
|
import com.android.settings.SettingsActivity;
|
|
import com.android.settings.core.PreferenceController;
|
|
import com.android.settings.dashboard.SummaryLoader;
|
|
import com.android.settings.location.ScanningSettings;
|
|
import com.android.settings.overlay.FeatureFactory;
|
|
import com.android.settings.search.BaseSearchIndexProvider;
|
|
import com.android.settings.search.Indexable;
|
|
import com.android.settings.search.SearchIndexableRaw;
|
|
import com.android.settings.widget.GearPreference;
|
|
import com.android.settings.widget.SummaryUpdater.OnSummaryChangeListener;
|
|
import com.android.settings.widget.SwitchBar;
|
|
import com.android.settings.widget.SwitchBarController;
|
|
import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
|
|
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
|
import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
|
|
import com.android.settingslib.bluetooth.LocalBluetoothManager;
|
|
import com.android.settingslib.core.AbstractPreferenceController;
|
|
import com.android.settingslib.core.lifecycle.Lifecycle;
|
|
import com.android.settingslib.widget.FooterPreference;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
|
|
|
|
/**
|
|
* BluetoothSettings is the Settings screen for Bluetooth configuration and
|
|
* connection management.
|
|
*/
|
|
public class BluetoothSettings extends DeviceListPreferenceFragment implements Indexable {
|
|
private static final String TAG = "BluetoothSettings";
|
|
private static final int PAIRED_DEVICE_ORDER = 1;
|
|
private static final int PAIRING_PREF_ORDER = 2;
|
|
|
|
@VisibleForTesting
|
|
static final String KEY_PAIRED_DEVICES = "paired_devices";
|
|
@VisibleForTesting
|
|
static final String KEY_FOOTER_PREF = "footer_preference";
|
|
|
|
@VisibleForTesting
|
|
PreferenceGroup mPairedDevicesCategory;
|
|
@VisibleForTesting
|
|
FooterPreference mFooterPreference;
|
|
private Preference mPairingPreference;
|
|
private BluetoothEnabler mBluetoothEnabler;
|
|
private AlwaysDiscoverable mAlwaysDiscoverable;
|
|
|
|
private SwitchBar mSwitchBar;
|
|
|
|
private BluetoothDeviceNamePreferenceController mDeviceNamePrefController;
|
|
@VisibleForTesting
|
|
BluetoothPairingPreferenceController mPairingPrefController;
|
|
|
|
// For Search
|
|
@VisibleForTesting
|
|
static final String DATA_KEY_REFERENCE = "main_toggle_bluetooth";
|
|
|
|
public BluetoothSettings() {
|
|
super(DISALLOW_CONFIG_BLUETOOTH);
|
|
}
|
|
|
|
@Override
|
|
public int getMetricsCategory() {
|
|
return MetricsEvent.BLUETOOTH;
|
|
}
|
|
|
|
@Override
|
|
public void onActivityCreated(Bundle savedInstanceState) {
|
|
super.onActivityCreated(savedInstanceState);
|
|
|
|
final SettingsActivity activity = (SettingsActivity) getActivity();
|
|
mSwitchBar = activity.getSwitchBar();
|
|
|
|
mBluetoothEnabler = new BluetoothEnabler(activity, new SwitchBarController(mSwitchBar),
|
|
mMetricsFeatureProvider, Utils.getLocalBtManager(activity),
|
|
MetricsEvent.ACTION_BLUETOOTH_TOGGLE);
|
|
mBluetoothEnabler.setupSwitchController();
|
|
if (mLocalAdapter != null) {
|
|
mAlwaysDiscoverable = new AlwaysDiscoverable(getContext(), mLocalAdapter);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onDestroyView() {
|
|
super.onDestroyView();
|
|
|
|
mBluetoothEnabler.teardownSwitchController();
|
|
}
|
|
|
|
@Override
|
|
void initPreferencesFromPreferenceScreen() {
|
|
mPairingPreference = mPairingPrefController.createBluetoothPairingPreference(
|
|
PAIRING_PREF_ORDER);
|
|
mFooterPreference = (FooterPreference) findPreference(KEY_FOOTER_PREF);
|
|
mPairedDevicesCategory = (PreferenceGroup) findPreference(KEY_PAIRED_DEVICES);
|
|
}
|
|
|
|
@Override
|
|
public void onStart() {
|
|
// resume BluetoothEnabler before calling super.onStart() so we don't get
|
|
// any onDeviceAdded() callbacks before setting up view in updateContent()
|
|
if (mBluetoothEnabler != null) {
|
|
mBluetoothEnabler.resume(getActivity());
|
|
}
|
|
super.onStart();
|
|
if (isUiRestricted()) {
|
|
getPreferenceScreen().removeAll();
|
|
if (!isUiRestrictedByOnlyAdmin()) {
|
|
getEmptyTextView().setText(R.string.bluetooth_empty_list_user_restricted);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (mLocalAdapter != null) {
|
|
updateContent(mLocalAdapter.getBluetoothState());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onStop() {
|
|
super.onStop();
|
|
if (mBluetoothEnabler != null) {
|
|
mBluetoothEnabler.pause();
|
|
}
|
|
|
|
// Make the device only visible to connected devices.
|
|
if (mAlwaysDiscoverable != null) {
|
|
mAlwaysDiscoverable.stop();
|
|
}
|
|
|
|
if (isUiRestricted()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String getDeviceListKey() {
|
|
return KEY_PAIRED_DEVICES;
|
|
}
|
|
|
|
private void updateContent(int bluetoothState) {
|
|
int messageId = 0;
|
|
|
|
switch (bluetoothState) {
|
|
case BluetoothAdapter.STATE_ON:
|
|
displayEmptyMessage(false);
|
|
mDevicePreferenceMap.clear();
|
|
|
|
if (isUiRestricted()) {
|
|
messageId = R.string.bluetooth_empty_list_user_restricted;
|
|
break;
|
|
}
|
|
|
|
addDeviceCategory(mPairedDevicesCategory,
|
|
R.string.bluetooth_preference_paired_devices,
|
|
BluetoothDeviceFilter.BONDED_DEVICE_FILTER, true);
|
|
mPairedDevicesCategory.addPreference(mPairingPreference);
|
|
updateFooterPreference(mFooterPreference);
|
|
|
|
if (mAlwaysDiscoverable != null) {
|
|
mAlwaysDiscoverable.start();
|
|
}
|
|
return; // not break
|
|
|
|
case BluetoothAdapter.STATE_TURNING_OFF:
|
|
messageId = R.string.bluetooth_turning_off;
|
|
mLocalAdapter.stopScanning();
|
|
break;
|
|
|
|
case BluetoothAdapter.STATE_OFF:
|
|
setOffMessage();
|
|
if (isUiRestricted()) {
|
|
messageId = R.string.bluetooth_empty_list_user_restricted;
|
|
}
|
|
break;
|
|
|
|
case BluetoothAdapter.STATE_TURNING_ON:
|
|
messageId = R.string.bluetooth_turning_on;
|
|
break;
|
|
}
|
|
|
|
displayEmptyMessage(true);
|
|
if (messageId != 0) {
|
|
getEmptyTextView().setText(messageId);
|
|
}
|
|
}
|
|
|
|
private void setOffMessage() {
|
|
final TextView emptyView = getEmptyTextView();
|
|
if (emptyView == null) {
|
|
return;
|
|
}
|
|
final CharSequence briefText = getText(R.string.bluetooth_empty_list_bluetooth_off);
|
|
|
|
final ContentResolver resolver = getActivity().getContentResolver();
|
|
final boolean bleScanningMode = Settings.Global.getInt(
|
|
resolver, Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE, 0) == 1;
|
|
|
|
if (!bleScanningMode) {
|
|
// Show only the brief text if the scanning mode has been turned off.
|
|
emptyView.setText(briefText, TextView.BufferType.SPANNABLE);
|
|
} else {
|
|
final StringBuilder contentBuilder = new StringBuilder();
|
|
contentBuilder.append(briefText);
|
|
contentBuilder.append("\n\n");
|
|
contentBuilder.append(getText(R.string.ble_scan_notify_text));
|
|
LinkifyUtils.linkify(emptyView, contentBuilder, new LinkifyUtils.OnClickListener() {
|
|
@Override
|
|
public void onClick() {
|
|
final SettingsActivity activity =
|
|
(SettingsActivity) BluetoothSettings.this.getActivity();
|
|
activity.startPreferencePanel(BluetoothSettings.this,
|
|
ScanningSettings.class.getName(), null,
|
|
R.string.location_scanning_screen_title, null, null, 0);
|
|
}
|
|
});
|
|
}
|
|
setTextSpan(emptyView.getText(), briefText);
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void displayEmptyMessage(boolean display) {
|
|
final Activity activity = getActivity();
|
|
activity.findViewById(android.R.id.list_container).setVisibility(
|
|
display ? View.INVISIBLE : View.VISIBLE);
|
|
activity.findViewById(android.R.id.empty).setVisibility(
|
|
display ? View.VISIBLE : View.GONE);
|
|
}
|
|
|
|
@Override
|
|
public void onBluetoothStateChanged(int bluetoothState) {
|
|
super.onBluetoothStateChanged(bluetoothState);
|
|
updateContent(bluetoothState);
|
|
}
|
|
|
|
@Override
|
|
public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
|
|
updateContent(mLocalAdapter.getBluetoothState());
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void setTextSpan(CharSequence text, CharSequence briefText) {
|
|
if (text instanceof Spannable) {
|
|
Spannable boldSpan = (Spannable) text;
|
|
boldSpan.setSpan(
|
|
new TextAppearanceSpan(getActivity(), android.R.style.TextAppearance_Medium), 0,
|
|
briefText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void setLocalBluetoothAdapter(LocalBluetoothAdapter localAdapter) {
|
|
mLocalAdapter = localAdapter;
|
|
}
|
|
|
|
private final GearPreference.OnGearClickListener mDeviceProfilesListener = pref -> {
|
|
// User clicked on advanced options icon for a device in the list
|
|
if (!(pref instanceof BluetoothDevicePreference)) {
|
|
Log.w(TAG, "onClick() called for other View: " + pref);
|
|
return;
|
|
}
|
|
final CachedBluetoothDevice device =
|
|
((BluetoothDevicePreference) pref).getBluetoothDevice();
|
|
if (device == null) {
|
|
Log.w(TAG, "No BT device attached with this pref: " + pref);
|
|
return;
|
|
}
|
|
final Bundle args = new Bundle();
|
|
Context context = getActivity();
|
|
boolean useDetailPage = FeatureFactory.getFactory(context).getBluetoothFeatureProvider(
|
|
context).isDeviceDetailPageEnabled();
|
|
if (!useDetailPage) {
|
|
// Old version - uses a dialog.
|
|
args.putString(DeviceProfilesSettings.ARG_DEVICE_ADDRESS,
|
|
device.getDevice().getAddress());
|
|
final DeviceProfilesSettings profileSettings = new DeviceProfilesSettings();
|
|
profileSettings.setArguments(args);
|
|
profileSettings.show(getFragmentManager(),
|
|
DeviceProfilesSettings.class.getSimpleName());
|
|
} else {
|
|
// New version - uses a separate screen.
|
|
args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS,
|
|
device.getDevice().getAddress());
|
|
final SettingsActivity activity =
|
|
(SettingsActivity) BluetoothSettings.this.getActivity();
|
|
activity.startPreferencePanel(this,
|
|
BluetoothDeviceDetailsFragment.class.getName(), args,
|
|
R.string.device_details_title, null, null, 0);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Add a listener, which enables the advanced settings icon.
|
|
*
|
|
* @param preference the newly added preference
|
|
*/
|
|
@Override
|
|
void initDevicePreference(BluetoothDevicePreference preference) {
|
|
preference.setOrder(PAIRED_DEVICE_ORDER);
|
|
CachedBluetoothDevice cachedDevice = preference.getCachedDevice();
|
|
if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
|
|
// Only paired device have an associated advanced settings screen
|
|
preference.setOnGearClickListener(mDeviceProfilesListener);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected int getHelpResource() {
|
|
return R.string.help_url_bluetooth;
|
|
}
|
|
|
|
@Override
|
|
protected String getLogTag() {
|
|
return TAG;
|
|
}
|
|
|
|
@Override
|
|
protected int getPreferenceScreenResId() {
|
|
return R.xml.bluetooth_settings;
|
|
}
|
|
|
|
@Override
|
|
protected List<PreferenceController> getPreferenceControllers(Context context) {
|
|
final List<PreferenceController> controllers = new ArrayList<>();
|
|
final Lifecycle lifecycle = getLifecycle();
|
|
mDeviceNamePrefController = new BluetoothDeviceNamePreferenceController(context, lifecycle);
|
|
mPairingPrefController = new BluetoothPairingPreferenceController(context, this,
|
|
(SettingsActivity) getActivity());
|
|
controllers.add(mDeviceNamePrefController);
|
|
controllers.add(mPairingPrefController);
|
|
controllers.add(new BluetoothFilesPreferenceController(context));
|
|
controllers.add(new BluetoothDeviceRenamePreferenceController(context, this, lifecycle));
|
|
|
|
return controllers;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
static class SummaryProvider implements SummaryLoader.SummaryProvider, OnSummaryChangeListener {
|
|
|
|
private final LocalBluetoothManager mBluetoothManager;
|
|
private final Context mContext;
|
|
private final SummaryLoader mSummaryLoader;
|
|
|
|
@VisibleForTesting
|
|
BluetoothSummaryUpdater mSummaryUpdater;
|
|
|
|
public SummaryProvider(Context context, SummaryLoader summaryLoader,
|
|
LocalBluetoothManager bluetoothManager) {
|
|
mBluetoothManager = bluetoothManager;
|
|
mContext = context;
|
|
mSummaryLoader = summaryLoader;
|
|
mSummaryUpdater = new BluetoothSummaryUpdater(mContext, this, mBluetoothManager);
|
|
}
|
|
|
|
@Override
|
|
public void setListening(boolean listening) {
|
|
mSummaryUpdater.register(listening);
|
|
}
|
|
|
|
@Override
|
|
public void onSummaryChanged(String summary) {
|
|
if (mSummaryLoader != null) {
|
|
mSummaryLoader.setSummary(this, summary);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY
|
|
= new SummaryLoader.SummaryProviderFactory() {
|
|
@Override
|
|
public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity,
|
|
SummaryLoader summaryLoader) {
|
|
|
|
return new SummaryProvider(activity, summaryLoader, Utils.getLocalBtManager(activity));
|
|
}
|
|
};
|
|
|
|
public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
|
|
new BaseSearchIndexProvider() {
|
|
@Override
|
|
public List<SearchIndexableRaw> getRawDataToIndex(Context context,
|
|
boolean enabled) {
|
|
|
|
final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>();
|
|
|
|
final Resources res = context.getResources();
|
|
|
|
// Add fragment title
|
|
SearchIndexableRaw data = new SearchIndexableRaw(context);
|
|
data.title = res.getString(R.string.bluetooth_settings);
|
|
data.screenTitle = res.getString(R.string.bluetooth_settings);
|
|
data.key = DATA_KEY_REFERENCE;
|
|
result.add(data);
|
|
|
|
// Removed paired bluetooth device indexing. See BluetoothSettingsObsolete.java.
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public List<String> getNonIndexableKeys(Context context) {
|
|
List<String> keys = super.getNonIndexableKeys(context);
|
|
if (!FeatureFactory.getFactory(context).getBluetoothFeatureProvider(
|
|
context).isPairingPageEnabled()) {
|
|
keys.add(DATA_KEY_REFERENCE);
|
|
}
|
|
return keys;
|
|
}
|
|
};
|
|
}
|