Merge tm-dev-plus-aosp-without-vendor@8763363

Bug: 236760014
Merged-In: Ifcb9d4c564839199d998bd503f390f021c6bf3ad
Change-Id: I9d69bcbc6916176beece2616f152ebd3d74fc0f8
This commit is contained in:
Xin Li
2022-06-28 21:23:28 +00:00
1671 changed files with 85400 additions and 30222 deletions

View File

@@ -99,6 +99,10 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
@VisibleForTesting
boolean mIsRegisterCallback = false;
@VisibleForTesting
boolean mIsLeftDeviceEstimateReady;
@VisibleForTesting
boolean mIsRightDeviceEstimateReady;
@VisibleForTesting
final BluetoothAdapter.OnMetadataChangedListener mMetadataListener =
new BluetoothAdapter.OnMetadataChangedListener() {
@Override
@@ -226,6 +230,8 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING,
R.string.bluetooth_right_name,
RIGHT_DEVICE_ID);
showBothDevicesBatteryPredictionIfNecessary();
}
}
}
@@ -365,8 +371,13 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
+ ", ESTIMATE_READY : " + estimateReady
+ ", BATTERY_ESTIMATE : " + batteryEstimate);
}
showBatteryPredictionIfNecessary(estimateReady, batteryEstimate,
linearLayout);
showBatteryPredictionIfNecessary(estimateReady, batteryEstimate, linearLayout);
if (batteryId == LEFT_DEVICE_ID) {
mIsLeftDeviceEstimateReady = estimateReady == 1;
} else if (batteryId == RIGHT_DEVICE_ID) {
mIsRightDeviceEstimateReady = estimateReady == 1;
}
}
} finally {
cursor.close();
@@ -380,7 +391,6 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
ThreadUtils.postOnMainThread(() -> {
final TextView textView = linearLayout.findViewById(R.id.bt_battery_prediction);
if (estimateReady == 1) {
textView.setVisibility(View.VISIBLE);
textView.setText(
StringUtil.formatElapsedTime(
mContext,
@@ -393,6 +403,24 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
});
}
@VisibleForTesting
void showBothDevicesBatteryPredictionIfNecessary() {
TextView leftDeviceTextView =
mLayoutPreference.findViewById(R.id.layout_left)
.findViewById(R.id.bt_battery_prediction);
TextView rightDeviceTextView =
mLayoutPreference.findViewById(R.id.layout_right)
.findViewById(R.id.bt_battery_prediction);
boolean isBothDevicesEstimateReady =
mIsLeftDeviceEstimateReady && mIsRightDeviceEstimateReady;
int visibility = isBothDevicesEstimateReady ? View.VISIBLE : View.GONE;
ThreadUtils.postOnMainThread(() -> {
leftDeviceTextView.setVisibility(visibility);
rightDeviceTextView.setVisibility(visibility);
});
}
private void showBatteryIcon(LinearLayout linearLayout, int level, int lowBatteryLevel,
boolean charging) {
final boolean enableLowBattery = level <= lowBatteryLevel && !charging;

View File

@@ -52,7 +52,8 @@ public class AlwaysDiscoverable extends BroadcastReceiver {
if (mStarted) {
return;
}
mContext.registerReceiver(this, mIntentFilter);
mContext.registerReceiver(this, mIntentFilter,
Context.RECEIVER_EXPORTED_UNAUDITED);
mStarted = true;
if (mBluetoothAdapter.getScanMode()
!= BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {

View File

@@ -19,105 +19,93 @@ package com.android.settings.bluetooth;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import com.android.settings.R;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import java.util.ArrayList;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.media.MediaOutputConstants;
/**
* This Dialog allowed users to do some actions for broadcast media or find the
* nearby broadcast sources.
*/
public class BluetoothBroadcastDialog extends InstrumentedDialogFragment {
public static final String KEY_APP_LABEL = "app_label";
public static final String KEY_DEVICE_ADDRESS =
BluetoothFindBroadcastsFragment.KEY_DEVICE_ADDRESS;
private static final String TAG = "BTBroadcastsDialog";
private static final CharSequence UNKNOWN_APP_LABEL = "unknown";
private Context mContext;
private CharSequence mCurrentAppLabel = UNKNOWN_APP_LABEL;
private String mDeviceAddress;
private LocalBluetoothManager mLocalBluetoothManager;
private AlertDialog mAlertDialog;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = getActivity();
mCurrentAppLabel = getActivity().getIntent().getCharSequenceExtra(KEY_APP_LABEL);
mDeviceAddress = getActivity().getIntent().getStringExtra(KEY_DEVICE_ADDRESS);
mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
setShowsDialog(true);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Context context = getActivity();
final boolean isMediaPlaying = isMediaPlaying();
View layout = View.inflate(mContext,
com.android.settingslib.R.layout.broadcast_dialog, null);
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(isMediaPlaying ? R.string.bluetooth_find_broadcast
: R.string.bluetooth_broadcast_dialog_title);
builder.setMessage(isMediaPlaying ? R.string.bluetooth_broadcast_dialog_find_message
: R.string.bluetooth_broadcast_dialog_broadcast_message);
TextView title = layout.findViewById(com.android.settingslib.R.id.dialog_title);
TextView subTitle = layout.findViewById(com.android.settingslib.R.id.dialog_subtitle);
title.setText(mContext.getString(R.string.bluetooth_broadcast_dialog_title));
subTitle.setText(
mContext.getString(R.string.bluetooth_broadcast_dialog_broadcast_message));
ArrayList<String> optionList = new ArrayList<String>();
if (!isMediaPlaying) {
optionList.add(context.getString(R.string.bluetooth_broadcast_dialog_title));
}
optionList.add(context.getString(R.string.bluetooth_find_broadcast));
optionList.add(context.getString(android.R.string.cancel));
View content = LayoutInflater.from(context).inflate(
R.layout.sim_confirm_dialog_multiple_enabled_profiles_supported, null);
if (content != null) {
Log.i(TAG, "list =" + optionList.toString());
final ArrayAdapter<String> arrayAdapterItems = new ArrayAdapter<String>(
context,
R.layout.sim_confirm_dialog_item_multiple_enabled_profiles_supported,
optionList);
final ListView lvItems = content.findViewById(R.id.carrier_list);
if (lvItems != null) {
lvItems.setVisibility(View.VISIBLE);
lvItems.setAdapter(arrayAdapterItems);
lvItems.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
Log.i(TAG, "list onClick =" + position);
Log.i(TAG, "list item =" + optionList.get(position));
if (position == optionList.size() - 1) {
// The last position in the options is the Cancel button. So when
// the user clicks the button, we do nothing but dismiss the dialog.
dismiss();
} else {
if (optionList.get(position).equals(
context.getString(R.string.bluetooth_find_broadcast))) {
launchFindBroadcastsActivity();
} else {
launchMediaOutputBroadcastDialog();
}
}
}
});
}
builder.setView(content);
Button broadcastBtn = layout.findViewById(com.android.settingslib.R.id.positive_btn);
if (TextUtils.isEmpty(mCurrentAppLabel)) {
broadcastBtn.setText(mContext.getString(R.string.bluetooth_broadcast_dialog_title));
} else {
Log.i(TAG, "optionList is empty");
broadcastBtn.setText(mContext.getString(
R.string.bluetooth_broadcast_dialog_broadcast_app,
String.valueOf(mCurrentAppLabel)));
}
broadcastBtn.setOnClickListener((view) -> {
launchMediaOutputBroadcastDialog();
});
AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
Button findBroadcastBtn = layout.findViewById(com.android.settingslib.R.id.negative_btn);
findBroadcastBtn.setText(mContext.getString(R.string.bluetooth_find_broadcast));
findBroadcastBtn.setOnClickListener((view) -> {
launchFindBroadcastsActivity();
});
Button cancelBtn = layout.findViewById(com.android.settingslib.R.id.neutral_btn);
cancelBtn.setOnClickListener((view) -> {
dismiss();
getActivity().finish();
});
mAlertDialog = new AlertDialog.Builder(mContext,
com.android.settingslib.R.style.Theme_AlertDialog_SettingsLib)
.setView(layout)
.create();
return mAlertDialog;
}
private boolean isMediaPlaying() {
return true;
}
@Override
public void onStart() {
super.onStart();
@@ -130,10 +118,55 @@ public class BluetoothBroadcastDialog extends InstrumentedDialogFragment {
}
private void launchFindBroadcastsActivity() {
Bundle bundle = new Bundle();
bundle.putString(KEY_DEVICE_ADDRESS, mDeviceAddress);
new SubSettingLauncher(mContext)
.setTitleRes(R.string.bluetooth_find_broadcast_title)
.setDestination(BluetoothFindBroadcastsFragment.class.getName())
.setArguments(bundle)
.setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
.launch();
dismissVolumePanel();
}
private void launchMediaOutputBroadcastDialog() {
if (startBroadcast()) {
mContext.sendBroadcast(new Intent()
.setPackage(MediaOutputConstants.SYSTEMUI_PACKAGE_NAME)
.setAction(MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG)
.putExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME,
getActivity().getPackageName()));
dismissVolumePanel();
}
}
private LocalBluetoothLeBroadcast getLEAudioBroadcastProfile() {
if (mLocalBluetoothManager != null && mLocalBluetoothManager.getProfileManager() != null) {
LocalBluetoothLeBroadcast bluetoothLeBroadcast =
mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
if (bluetoothLeBroadcast != null) {
return bluetoothLeBroadcast;
}
}
Log.d(TAG, "Can not get LE Audio Broadcast Profile");
return null;
}
private boolean startBroadcast() {
LocalBluetoothLeBroadcast btLeBroadcast = getLEAudioBroadcastProfile();
if (btLeBroadcast != null) {
btLeBroadcast.startBroadcast(String.valueOf(mCurrentAppLabel), null);
return true;
}
Log.d(TAG, "Can not broadcast successfully");
return false;
}
private void dismissVolumePanel() {
// Dismiss volume panel
mContext.sendBroadcast(new Intent()
.setPackage(MediaOutputConstants.SETTINGS_PACKAGE_NAME)
.setAction(MediaOutputConstants.ACTION_CLOSE_PANEL));
}
}

View File

@@ -0,0 +1,183 @@
/*
* 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.bluetooth;
import android.bluetooth.BluetoothLeAudioContentMetadata;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothLeBroadcastSubgroup;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settingslib.Utils;
import java.util.List;
/**
* Preference to display a broadcast source in the Broadcast Source List.
*/
class BluetoothBroadcastSourcePreference extends Preference {
private static final int RESOURCE_ID_UNKNOWN_PROGRAM_INFO = R.string.device_info_default;
private static final int RESOURCE_ID_ICON = R.drawable.settings_input_antenna;
private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata;
private BluetoothLeBroadcastReceiveState mBluetoothLeBroadcastReceiveState;
private ImageView mFrictionImageView;
private String mTitle;
private boolean mStatus;
private boolean mIsEncrypted;
BluetoothBroadcastSourcePreference(@NonNull Context context) {
super(context);
initUi();
}
@Override
public void onBindViewHolder(final PreferenceViewHolder view) {
super.onBindViewHolder(view);
view.findViewById(R.id.two_target_divider).setVisibility(View.INVISIBLE);
final ImageButton imageButton = (ImageButton) view.findViewById(R.id.icon_button);
imageButton.setVisibility(View.GONE);
mFrictionImageView = (ImageView) view.findViewById(R.id.friction_icon);
updateStatusButton();
}
private void initUi() {
setLayoutResource(R.layout.preference_access_point);
setWidgetLayoutResource(R.layout.access_point_friction_widget);
mTitle = getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO);
mStatus = false;
final Drawable drawable = getContext().getDrawable(RESOURCE_ID_ICON);
if (drawable != null) {
drawable.setTint(Utils.getColorAttrDefaultColor(getContext(),
android.R.attr.colorControlNormal));
setIcon(drawable);
}
}
private void updateStatusButton() {
if (mFrictionImageView == null) {
return;
}
if (mStatus || mIsEncrypted) {
Drawable drawable;
if (mStatus) {
drawable = getContext().getDrawable(R.drawable.bluetooth_broadcast_dialog_done);
} else {
drawable = getContext().getDrawable(R.drawable.ic_friction_lock_closed);
}
if (drawable != null) {
drawable.setTint(Utils.getColorAttrDefaultColor(getContext(),
android.R.attr.colorControlNormal));
mFrictionImageView.setImageDrawable(drawable);
}
mFrictionImageView.setVisibility(View.VISIBLE);
} else {
mFrictionImageView.setVisibility(View.GONE);
}
}
/**
* Updates the title and status from BluetoothLeBroadcastMetadata.
*/
public void updateMetadataAndRefreshUi(BluetoothLeBroadcastMetadata source, boolean status) {
mBluetoothLeBroadcastMetadata = source;
mTitle = getProgramInfo();
mIsEncrypted = mBluetoothLeBroadcastMetadata.isEncrypted();
mStatus = status || mBluetoothLeBroadcastReceiveState != null;
refresh();
}
/**
* Updates the title and status from BluetoothLeBroadcastReceiveState.
*/
public void updateReceiveStateAndRefreshUi(BluetoothLeBroadcastReceiveState receiveState) {
mBluetoothLeBroadcastReceiveState = receiveState;
mTitle = getProgramInfo();
mStatus = true;
refresh();
}
/**
* Gets the BluetoothLeBroadcastMetadata.
*/
public BluetoothLeBroadcastMetadata getBluetoothLeBroadcastMetadata() {
return mBluetoothLeBroadcastMetadata;
}
private void refresh() {
setTitle(mTitle);
updateStatusButton();
}
private String getProgramInfo() {
if (mBluetoothLeBroadcastReceiveState != null) {
List<BluetoothLeAudioContentMetadata> bluetoothLeAudioContentMetadata =
mBluetoothLeBroadcastReceiveState.getSubgroupMetadata();
if (!bluetoothLeAudioContentMetadata.isEmpty()) {
return bluetoothLeAudioContentMetadata.stream()
.map(i -> i.getProgramInfo())
.findFirst().orElse(
getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO));
}
}
if (mBluetoothLeBroadcastMetadata == null) {
return getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO);
}
final List<BluetoothLeBroadcastSubgroup> subgroups =
mBluetoothLeBroadcastMetadata.getSubgroups();
if (subgroups.isEmpty()) {
return getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO);
}
return subgroups.stream()
.map(i -> i.getContentMetadata().getProgramInfo())
.filter(i -> !TextUtils.isEmpty(i))
.findFirst().orElse(getContext().getString(RESOURCE_ID_UNKNOWN_PROGRAM_INFO));
}
/**
* Whether the broadcast source is encrypted or not.
* @return If true, the broadcast source needs the broadcast code. If false, the broadcast
* source does not need the broadcast code.
*/
public boolean isEncrypted() {
return mIsEncrypted;
}
/**
* Clear the BluetoothLeBroadcastReceiveState and reset the state when the user clicks the
* "leave broadcast" button.
*/
public void clearReceiveState() {
mBluetoothLeBroadcastReceiveState = null;
mTitle = getProgramInfo();
mStatus = false;
refresh();
}
}

View File

@@ -16,6 +16,7 @@
package com.android.settings.bluetooth;
import android.app.settings.SettingsEnums;
import android.content.Context;
import androidx.preference.PreferenceFragmentCompat;
@@ -70,7 +71,11 @@ public class BluetoothDetailsButtonsController extends BluetoothDetailsControlle
mActionButtons
.setButton2Text(R.string.bluetooth_device_context_disconnect)
.setButton2Icon(R.drawable.ic_settings_close)
.setButton2OnClickListener(view -> mCachedDevice.disconnect());
.setButton2OnClickListener(view -> {
mMetricsFeatureProvider.action(mContext,
SettingsEnums.ACTION_SETTINGS_BLUETOOTH_DISCONNECT);
mCachedDevice.disconnect();
});
mConnectButtonInitialized = true;
}
} else {
@@ -79,7 +84,11 @@ public class BluetoothDetailsButtonsController extends BluetoothDetailsControlle
.setButton2Text(R.string.bluetooth_device_context_connect)
.setButton2Icon(R.drawable.ic_add_24dp)
.setButton2OnClickListener(
view -> mCachedDevice.connect());
view -> {
mMetricsFeatureProvider.action(mContext,
SettingsEnums.ACTION_SETTINGS_BLUETOOTH_CONNECT);
mCachedDevice.connect();
});
mConnectButtonInitialized = true;
}
}

View File

@@ -18,7 +18,7 @@ package com.android.settings.bluetooth;
import static com.android.internal.util.CollectionUtils.filter;
import android.companion.Association;
import android.companion.AssociationInfo;
import android.companion.CompanionDeviceManager;
import android.companion.ICompanionDeviceManager;
import android.content.Context;
@@ -29,6 +29,7 @@ import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.Log;
@@ -88,7 +89,7 @@ public class BluetoothDetailsCompanionAppsController extends BluetoothDetailsCon
mProfilesContainer.setLayoutResource(R.layout.preference_companion_app);
}
private List<Association> getAssociations(String address) {
private List<AssociationInfo> getAssociations(String address) {
return filter(
mCompanionDeviceManager.getAllAssociations(),
a -> Objects.equal(address, a.getDeviceMacAddress()));
@@ -126,8 +127,8 @@ public class BluetoothDetailsCompanionAppsController extends BluetoothDetailsCon
try {
java.util.Objects.requireNonNull(ICompanionDeviceManager.Stub.asInterface(
ServiceManager.getService(
Context.COMPANION_DEVICE_SERVICE))).disassociate(
address, packageName);
Context.COMPANION_DEVICE_SERVICE))).legacyDisassociate(
address, packageName, UserHandle.myUserId());
} catch (RemoteException e) {
throw new RuntimeException(e);
}
@@ -150,7 +151,7 @@ public class BluetoothDetailsCompanionAppsController extends BluetoothDetailsCon
private List<String> getPreferencesNeedToShow(String address, PreferenceCategory container) {
List<String> preferencesToRemove = new ArrayList<>();
Set<String> packages = getAssociations(address)
.stream().map(Association::getPackageName)
.stream().map(AssociationInfo::getPackageName)
.collect(Collectors.toSet());
for (int i = 0; i < container.getPreferenceCount(); i++) {
@@ -185,7 +186,7 @@ public class BluetoothDetailsCompanionAppsController extends BluetoothDetailsCon
String address, PreferenceCategory container) {
// If the device is FastPair, remove CDM companion apps.
final BluetoothFeatureProvider bluetoothFeatureProvider = FeatureFactory.getFactory(context)
.getBluetoothFeatureProvider(context);
.getBluetoothFeatureProvider();
final boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true);
final Uri settingsUri = bluetoothFeatureProvider.getBluetoothDeviceSettingsUri(

View File

@@ -22,8 +22,10 @@ import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnPause;
@@ -40,6 +42,7 @@ public abstract class BluetoothDetailsController extends AbstractPreferenceContr
protected final Context mContext;
protected final PreferenceFragmentCompat mFragment;
protected final CachedBluetoothDevice mCachedDevice;
protected final MetricsFeatureProvider mMetricsFeatureProvider;
public BluetoothDetailsController(Context context, PreferenceFragmentCompat fragment,
CachedBluetoothDevice device, Lifecycle lifecycle) {
@@ -48,6 +51,7 @@ public abstract class BluetoothDetailsController extends AbstractPreferenceContr
mFragment = fragment;
mCachedDevice = device;
lifecycle.addObserver(this);
mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
@Override

View File

@@ -75,10 +75,8 @@ public class BluetoothDetailsHeaderController extends BluetoothDetailsController
if (TextUtils.isEmpty(summaryText)) {
// If first summary is unavailable, not to show second summary.
mHeaderController.setSecondSummary((CharSequence)null);
} else {
// If both the hearing aids are connected, two device status should be shown.
mHeaderController.setSecondSummary(mDeviceManager.getSubDeviceSummary(mCachedDevice));
}
mHeaderController.setLabel(mCachedDevice.getName());
mHeaderController.setIcon(pair.first);
mHeaderController.setIconContentDescription(pair.second);

View File

@@ -488,7 +488,7 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
@Override
protected void refresh() {
for (LocalBluetoothProfile profile : getProfiles()) {
if (!profile.isProfileReady()) {
if (profile == null || !profile.isProfileReady()) {
continue;
}
SwitchPreference pref = mProfilesContainer.findPreference(

View File

@@ -0,0 +1,124 @@
/*
* 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.bluetooth;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.AccessibilityShortcutInfo;
import android.content.ComponentName;
import android.content.Context;
import android.os.UserHandle;
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import com.android.net.module.util.CollectionUtils;
import com.android.settings.accessibility.RestrictedPreferenceHelper;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.core.lifecycle.Lifecycle;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* This class adds related tools preference.
*/
public class BluetoothDetailsRelatedToolsController extends BluetoothDetailsController{
private static final String KEY_RELATED_TOOLS_GROUP = "bluetooth_related_tools";
private static final String KEY_LIVE_CAPTION = "live_caption";
private static final int ORDINAL = 99;
private PreferenceCategory mPreferenceCategory;
public BluetoothDetailsRelatedToolsController(Context context,
PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle) {
super(context, fragment, device, lifecycle);
lifecycle.addObserver(this);
}
@Override
public boolean isAvailable() {
return mCachedDevice.isHearingAidDevice();
}
@Override
protected void init(PreferenceScreen screen) {
if (!mCachedDevice.isHearingAidDevice()) {
return;
}
mPreferenceCategory = screen.findPreference(getPreferenceKey());
final Preference liveCaptionPreference = screen.findPreference(KEY_LIVE_CAPTION);
if (!liveCaptionPreference.isVisible()) {
mPreferenceCategory.removePreference(liveCaptionPreference);
}
final List<ComponentName> relatedToolsList = FeatureFactory.getFactory(
mContext).getBluetoothFeatureProvider().getRelatedTools();
if (!CollectionUtils.isEmpty(relatedToolsList)) {
addAccessibilityInstalledRelatedPreference(relatedToolsList);
}
if (mPreferenceCategory.getPreferenceCount() == 0) {
screen.removePreference(mPreferenceCategory);
}
}
@Override
protected void refresh() {}
@Override
public String getPreferenceKey() {
return KEY_RELATED_TOOLS_GROUP;
}
private void addAccessibilityInstalledRelatedPreference(
@NonNull List<ComponentName> componentNameList) {
final AccessibilityManager a11yManager = AccessibilityManager.getInstance(mContext);
final RestrictedPreferenceHelper preferenceHelper = new RestrictedPreferenceHelper(
mContext);
final List<AccessibilityServiceInfo> a11yServiceInfoList =
a11yManager.getInstalledAccessibilityServiceList().stream()
.filter(info -> componentNameList.contains(info.getComponentName()))
.collect(Collectors.toList());
final List<AccessibilityShortcutInfo> a11yShortcutInfoList =
a11yManager.getInstalledAccessibilityShortcutListAsUser(mContext,
UserHandle.myUserId()).stream()
.filter(info -> componentNameList.contains(info.getComponentName()))
.collect(Collectors.toList());
final List<RestrictedPreference> preferences = Stream.of(
preferenceHelper.createAccessibilityServicePreferenceList(a11yServiceInfoList),
preferenceHelper.createAccessibilityActivityPreferenceList(a11yShortcutInfoList))
.flatMap(Collection::stream)
.collect(Collectors.toList());
for (RestrictedPreference preference : preferences) {
preference.setOrder(ORDINAL);
mPreferenceCategory.addPreference(preference);
}
}
}

View File

@@ -0,0 +1,155 @@
/*
* 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.bluetooth;
import android.content.Context;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.Spatializer;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;
import com.android.settings.R;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.core.lifecycle.Lifecycle;
/**
* The controller of the Spatial audio setting in the bluetooth detail settings.
*/
public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsController
implements Preference.OnPreferenceClickListener {
private static final String TAG = "BluetoothSpatialAudioController";
private static final String KEY_SPATIAL_AUDIO_GROUP = "spatial_audio_group";
private static final String KEY_SPATIAL_AUDIO = "spatial_audio";
private static final String KEY_HEAD_TRACKING = "head_tracking";
private final Spatializer mSpatializer;
@VisibleForTesting
PreferenceCategory mProfilesContainer;
@VisibleForTesting
AudioDeviceAttributes mAudioDevice;
public BluetoothDetailsSpatialAudioController(
Context context,
PreferenceFragmentCompat fragment,
CachedBluetoothDevice device,
Lifecycle lifecycle) {
super(context, fragment, device, lifecycle);
AudioManager audioManager = context.getSystemService(AudioManager.class);
mSpatializer = audioManager.getSpatializer();
mAudioDevice = new AudioDeviceAttributes(
AudioDeviceAttributes.ROLE_OUTPUT,
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
mCachedDevice.getAddress());
}
@Override
public boolean isAvailable() {
return mSpatializer.isAvailableForDevice(mAudioDevice) ? true : false;
}
@Override
public boolean onPreferenceClick(Preference preference) {
SwitchPreference switchPreference = (SwitchPreference) preference;
String key = switchPreference.getKey();
if (TextUtils.equals(key, KEY_SPATIAL_AUDIO)) {
if (switchPreference.isChecked()) {
mSpatializer.addCompatibleAudioDevice(mAudioDevice);
} else {
mSpatializer.removeCompatibleAudioDevice(mAudioDevice);
}
refresh();
return true;
} else if (TextUtils.equals(key, KEY_HEAD_TRACKING)) {
mSpatializer.setHeadTrackerEnabled(switchPreference.isChecked(), mAudioDevice);
return true;
} else {
Log.w(TAG, "invalid key name.");
return false;
}
}
@Override
public String getPreferenceKey() {
return KEY_SPATIAL_AUDIO_GROUP;
}
@Override
protected void init(PreferenceScreen screen) {
mProfilesContainer = screen.findPreference(getPreferenceKey());
mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category);
refresh();
}
@Override
protected void refresh() {
SwitchPreference spatialAudioPref = mProfilesContainer.findPreference(KEY_SPATIAL_AUDIO);
if (spatialAudioPref == null) {
spatialAudioPref = createSpatialAudioPreference(mProfilesContainer.getContext());
mProfilesContainer.addPreference(spatialAudioPref);
}
boolean isSpatialAudioOn = mSpatializer.getCompatibleAudioDevices().contains(mAudioDevice);
Log.d(TAG, "refresh() isSpatialAudioOn : " + isSpatialAudioOn);
spatialAudioPref.setChecked(isSpatialAudioOn);
SwitchPreference headTrackingPref = mProfilesContainer.findPreference(KEY_HEAD_TRACKING);
if (headTrackingPref == null) {
headTrackingPref = createHeadTrackingPreference(mProfilesContainer.getContext());
mProfilesContainer.addPreference(headTrackingPref);
}
boolean isHeadTrackingAvailable =
isSpatialAudioOn && mSpatializer.hasHeadTracker(mAudioDevice);
Log.d(TAG, "refresh() has head tracker : " + mSpatializer.hasHeadTracker(mAudioDevice));
headTrackingPref.setVisible(isHeadTrackingAvailable);
if (isHeadTrackingAvailable) {
headTrackingPref.setChecked(mSpatializer.isHeadTrackerEnabled(mAudioDevice));
}
}
@VisibleForTesting
SwitchPreference createSpatialAudioPreference(Context context) {
SwitchPreference pref = new SwitchPreference(context);
pref.setKey(KEY_SPATIAL_AUDIO);
pref.setTitle(context.getString(R.string.bluetooth_details_spatial_audio_title));
pref.setSummary(context.getString(R.string.bluetooth_details_spatial_audio_summary));
pref.setOnPreferenceClickListener(this);
return pref;
}
@VisibleForTesting
SwitchPreference createHeadTrackingPreference(Context context) {
SwitchPreference pref = new SwitchPreference(context);
pref.setKey(KEY_HEAD_TRACKING);
pref.setTitle(context.getString(R.string.bluetooth_details_head_tracking_title));
pref.setSummary(context.getString(R.string.bluetooth_details_head_tracking_summary));
pref.setOnPreferenceClickListener(this);
return pref;
}
}

View File

@@ -22,12 +22,19 @@ import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.res.TypedArray;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import androidx.annotation.VisibleForTesting;
@@ -36,6 +43,7 @@ import com.android.settings.core.SettingsUIDeviceConfig;
import com.android.settings.dashboard.RestrictedDashboardFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.slices.BlockingSlicePrefController;
import com.android.settings.slices.SlicePreferenceController;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.AbstractPreferenceController;
@@ -61,6 +69,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
@VisibleForTesting
interface TestDataFactory {
CachedBluetoothDevice getDevice(String deviceAddress);
LocalBluetoothManager getManager(Context context);
}
@@ -120,7 +129,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
use(LeAudioBluetoothDetailsHeaderController.class).init(mCachedDevice, mManager);
final BluetoothFeatureProvider featureProvider = FeatureFactory.getFactory(
context).getBluetoothFeatureProvider(context);
context).getBluetoothFeatureProvider();
final boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true);
@@ -129,6 +138,52 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
: null);
}
private void updateExtraControlUri(int viewWidth) {
BluetoothFeatureProvider featureProvider = FeatureFactory.getFactory(
getContext()).getBluetoothFeatureProvider();
boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true);
Uri controlUri = null;
String uri = featureProvider.getBluetoothDeviceControlUri(mCachedDevice.getDevice());
if (!TextUtils.isEmpty(uri)) {
try {
controlUri = Uri.parse(uri + viewWidth);
} catch (NullPointerException exception) {
Log.d(TAG, "unable to parse uri");
controlUri = null;
}
}
use(SlicePreferenceController.class).setSliceUri(sliceEnabled ? controlUri : null);
use(SlicePreferenceController.class).onStart();
}
private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener =
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
View view = getView();
if (view == null) {
return;
}
if (view.getWidth() <= 0) {
return;
}
updateExtraControlUri(view.getWidth() - getPaddingSize());
view.getViewTreeObserver().removeOnGlobalLayoutListener(
mOnGlobalLayoutListener);
}
};
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
if (view != null) {
view.getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
}
return view;
}
@Override
public void onResume() {
super.onResume();
@@ -188,11 +243,28 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
lifecycle));
controllers.add(new BluetoothDetailsCompanionAppsController(context, this,
mCachedDevice, lifecycle));
controllers.add(new BluetoothDetailsSpatialAudioController(context, this, mCachedDevice,
lifecycle));
controllers.add(new BluetoothDetailsProfilesController(context, this, mManager,
mCachedDevice, lifecycle));
controllers.add(new BluetoothDetailsMacAddressController(context, this, mCachedDevice,
lifecycle));
controllers.add(new BluetoothDetailsRelatedToolsController(context, this, mCachedDevice,
lifecycle));
}
return controllers;
}
private int getPaddingSize() {
TypedArray resolvedAttributes =
getContext().obtainStyledAttributes(
new int[]{
android.R.attr.listPreferredItemPaddingStart,
android.R.attr.listPreferredItemPaddingEnd
});
int width = resolvedAttributes.getDimensionPixelSize(0, 0)
+ resolvedAttributes.getDimensionPixelSize(1, 0);
resolvedAttributes.recycle();
return width;
}
}

View File

@@ -31,6 +31,8 @@ import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settingslib.bluetooth.BluetoothDiscoverableTimeoutReceiver;
import java.time.Duration;
/**
* BluetoothDiscoverableEnabler is a helper to manage the "Discoverable"
* checkbox. It sets/unsets discoverability and keeps track of how much time
@@ -136,9 +138,8 @@ final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClick
int timeout = getDiscoverableTimeout();
long endTimestamp = System.currentTimeMillis() + timeout * 1000L;
LocalBluetoothPreferences.persistDiscoverableEndTimestamp(mContext, endTimestamp);
mBluetoothAdapter
.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, timeout);
mBluetoothAdapter.setDiscoverableTimeout(Duration.ofSeconds(timeout));
mBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
updateCountdownSummary();
Log.d(TAG, "setEnabled(): enabled = " + enable + "timeout = " + timeout);

View File

@@ -115,7 +115,8 @@ public final class BluetoothEnabler implements SwitchWidgetController.OnSwitchCh
}
mSwitchController.startListening();
mContext.registerReceiver(mReceiver, mIntentFilter);
mContext.registerReceiver(mReceiver, mIntentFilter,
Context.RECEIVER_EXPORTED_UNAUDITED);
mValidListener = true;
}

View File

@@ -17,17 +17,37 @@
package com.android.settings.bluetooth;
import android.bluetooth.BluetoothDevice;
import android.content.ComponentName;
import android.net.Uri;
import java.util.List;
/**
* Provider for bluetooth related feature
* Provider for bluetooth related features.
*/
public interface BluetoothFeatureProvider {
/**
* Get the {@link Uri} that represents extra settings for a specific bluetooth device
* Gets the {@link Uri} that represents extra settings for a specific bluetooth device
*
* @param bluetoothDevice bluetooth device
* @return {@link Uri} for extra settings
*/
Uri getBluetoothDeviceSettingsUri(BluetoothDevice bluetoothDevice);
/**
* Gets the {@link Uri} that represents extra control for a specific bluetooth device
*
* @param bluetoothDevice bluetooth device
* @return {@link String} uri string for extra control
*/
String getBluetoothDeviceControlUri(BluetoothDevice bluetoothDevice);
/**
* Gets the {@link ComponentName} of services or activities that need to be shown in related
* tools.
*
* @return list of {@link ComponentName}
*/
List<ComponentName> getRelatedTools();
}

View File

@@ -17,19 +17,20 @@
package com.android.settings.bluetooth;
import android.bluetooth.BluetoothDevice;
import android.content.ComponentName;
import android.content.Context;
import android.net.Uri;
import com.android.settingslib.bluetooth.BluetoothUtils;
import java.util.List;
/**
* Impl of {@link BluetoothFeatureProvider}
*/
public class BluetoothFeatureProviderImpl implements BluetoothFeatureProvider {
private Context mContext;
public BluetoothFeatureProviderImpl(Context context) {
mContext = context;
}
public BluetoothFeatureProviderImpl(Context context) {}
@Override
public Uri getBluetoothDeviceSettingsUri(BluetoothDevice bluetoothDevice) {
@@ -37,4 +38,14 @@ public class BluetoothFeatureProviderImpl implements BluetoothFeatureProvider {
BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI);
return uriByte == null ? null : Uri.parse(new String(uriByte));
}
@Override
public String getBluetoothDeviceControlUri(BluetoothDevice bluetoothDevice) {
return BluetoothUtils.getControlUriMetaData(bluetoothDevice);
}
@Override
public List<ComponentName> getRelatedTools() {
return null;
}
}

View File

@@ -19,33 +19,53 @@ package com.android.settings.bluetooth;
import static android.bluetooth.BluetoothDevice.BOND_NONE;
import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
import android.app.AlertDialog;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import com.android.settings.R;
import com.android.settings.dashboard.RestrictedDashboardFragment;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
* This fragment allowed users to find the nearby broadcast sources.
*/
public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment {
private static final String TAG = "BTFindBroadcastsFrg";
private static final String TAG = "BtFindBroadcastsFrg";
public static final String KEY_DEVICE_ADDRESS = "device_address";
public static final String PREF_KEY_BROADCAST_SOURCE = "broadcast_source";
public static final String PREF_KEY_BROADCAST_SOURCE_LIST = "broadcast_source_list";
@VisibleForTesting
String mDeviceAddress;
@@ -53,6 +73,91 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment
LocalBluetoothManager mManager;
@VisibleForTesting
CachedBluetoothDevice mCachedDevice;
@VisibleForTesting
PreferenceCategory mBroadcastSourceListCategory;
BluetoothFindBroadcastsHeaderController mBluetoothFindBroadcastsHeaderController;
private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
private BluetoothBroadcastSourcePreference mSelectedPreference;
private Executor mExecutor;
private int mSourceId;
private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
new BluetoothLeBroadcastAssistant.Callback() {
@Override
public void onSearchStarted(int reason) {
Log.d(TAG, "onSearchStarted: " + reason);
getActivity().runOnUiThread(() -> handleSearchStarted());
}
@Override
public void onSearchStartFailed(int reason) {
Log.d(TAG, "onSearchStartFailed: " + reason);
}
@Override
public void onSearchStopped(int reason) {
Log.d(TAG, "onSearchStopped: " + reason);
}
@Override
public void onSearchStopFailed(int reason) {
Log.d(TAG, "onSearchStopFailed: " + reason);
}
@Override
public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {
Log.d(TAG, "onSourceFound:");
getActivity().runOnUiThread(
() -> updateListCategoryFromBroadcastMetadata(source, false));
}
@Override
public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
setSourceId(sourceId);
if (mSelectedPreference == null) {
Log.w(TAG, "onSourceAdded: mSelectedPreference == null!");
return;
}
getActivity().runOnUiThread(() -> updateListCategoryFromBroadcastMetadata(
mSelectedPreference.getBluetoothLeBroadcastMetadata(), true));
}
@Override
public void onSourceAddFailed(@NonNull BluetoothDevice sink,
@NonNull BluetoothLeBroadcastMetadata source, int reason) {
mSelectedPreference = null;
Log.d(TAG, "onSourceAddFailed: clear the mSelectedPreference.");
}
@Override
public void onSourceModified(@NonNull BluetoothDevice sink, int sourceId,
int reason) {
}
@Override
public void onSourceModifyFailed(@NonNull BluetoothDevice sink, int sourceId,
int reason) {
}
@Override
public void onSourceRemoved(@NonNull BluetoothDevice sink, int sourceId,
int reason) {
Log.d(TAG, "onSourceRemoved:");
getActivity().runOnUiThread(() -> handleSourceRemoved());
}
@Override
public void onSourceRemoveFailed(@NonNull BluetoothDevice sink, int sourceId,
int reason) {
Log.d(TAG, "onSourceRemoveFailed:");
}
@Override
public void onReceiveStateChanged(@NonNull BluetoothDevice sink, int sourceId,
@NonNull BluetoothLeBroadcastReceiveState state) {
Log.d(TAG, "onReceiveStateChanged:");
}
};
public BluetoothFindBroadcastsFragment() {
super(DISALLOW_CONFIG_BLUETOOTH);
@@ -75,19 +180,52 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment
mDeviceAddress = getArguments().getString(KEY_DEVICE_ADDRESS);
mManager = getLocalBluetoothManager(context);
mCachedDevice = getCachedDevice(mDeviceAddress);
mLeBroadcastAssistant = getLeBroadcastAssistant();
mExecutor = Executors.newSingleThreadExecutor();
super.onAttach(context);
if (mCachedDevice == null) {
if (mCachedDevice == null || mLeBroadcastAssistant == null) {
//Close this page if device is null with invalid device mac address
Log.w(TAG, "onAttach() CachedDevice is null!");
//or if the device does not have LeBroadcastAssistant profile
Log.w(TAG, "onAttach() CachedDevice or LeBroadcastAssistant is null!");
finish();
return;
}
}
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
mBroadcastSourceListCategory = findPreference(PREF_KEY_BROADCAST_SOURCE_LIST);
}
@Override
public void onStart() {
super.onStart();
if (mLeBroadcastAssistant != null) {
mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
}
}
@Override
public void onResume() {
super.onResume();
finishFragmentIfNecessary();
//check assistant status. Start searching...
if (mLeBroadcastAssistant != null && !mLeBroadcastAssistant.isSearchInProgress()) {
mLeBroadcastAssistant.startSearchingForSources(getScanFilter());
} else {
addConnectedSourcePreference();
}
}
@Override
public void onStop() {
super.onStop();
if (mLeBroadcastAssistant != null) {
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
}
}
@VisibleForTesting
@@ -104,6 +242,28 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment
return SettingsEnums.PAGE_UNKNOWN;
}
/**
* Starts to scan broadcast source by the BluetoothLeBroadcastAssistant.
*/
public void scanBroadcastSource() {
if (mLeBroadcastAssistant == null) {
Log.w(TAG, "scanBroadcastSource: LeBroadcastAssistant is null!");
return;
}
mLeBroadcastAssistant.startSearchingForSources(getScanFilter());
}
/**
* Leaves the broadcast source by the BluetoothLeBroadcastAssistant.
*/
public void leaveBroadcastSession() {
if (mLeBroadcastAssistant == null || mCachedDevice == null) {
Log.w(TAG, "leaveBroadcastSession: LeBroadcastAssistant or CachedDevice is null!");
return;
}
mLeBroadcastAssistant.removeSource(mCachedDevice.getDevice(), getSourceId());
}
@Override
protected String getLogTag() {
return TAG;
@@ -120,9 +280,178 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment
if (mCachedDevice != null) {
Lifecycle lifecycle = getSettingsLifecycle();
controllers.add(new BluetoothFindBroadcastsHeaderController(context, this,
mCachedDevice, lifecycle, mManager));
mBluetoothFindBroadcastsHeaderController = new BluetoothFindBroadcastsHeaderController(
context, this, mCachedDevice, lifecycle, mManager);
controllers.add(mBluetoothFindBroadcastsHeaderController);
}
return controllers;
}
/**
* Gets the LocalBluetoothLeBroadcastAssistant
* @return the LocalBluetoothLeBroadcastAssistant
*/
public LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
if (mManager == null) {
Log.w(TAG, "getLeBroadcastAssistant: LocalBluetoothManager is null!");
return null;
}
LocalBluetoothProfileManager profileManager = mManager.getProfileManager();
if (profileManager == null) {
Log.w(TAG, "getLeBroadcastAssistant: LocalBluetoothProfileManager is null!");
return null;
}
return profileManager.getLeAudioBroadcastAssistantProfile();
}
private List<ScanFilter> getScanFilter() {
// Currently there is no function for setting the ScanFilter. It may have this function
// in the further.
return Collections.emptyList();
}
private void updateListCategoryFromBroadcastMetadata(BluetoothLeBroadcastMetadata source,
boolean isConnected) {
BluetoothBroadcastSourcePreference item = mBroadcastSourceListCategory.findPreference(
Integer.toString(source.getBroadcastId()));
if (item == null) {
item = createBluetoothBroadcastSourcePreference();
item.setKey(Integer.toString(source.getBroadcastId()));
mBroadcastSourceListCategory.addPreference(item);
}
item.updateMetadataAndRefreshUi(source, isConnected);
item.setOrder(isConnected ? 0 : 1);
//refresh the header
if (mBluetoothFindBroadcastsHeaderController != null) {
mBluetoothFindBroadcastsHeaderController.refreshUi();
}
}
private void updateListCategoryFromBroadcastReceiveState(
BluetoothLeBroadcastReceiveState receiveState) {
BluetoothBroadcastSourcePreference item = mBroadcastSourceListCategory.findPreference(
Integer.toString(receiveState.getBroadcastId()));
if (item == null) {
item = createBluetoothBroadcastSourcePreference();
item.setKey(Integer.toString(receiveState.getBroadcastId()));
mBroadcastSourceListCategory.addPreference(item);
}
item.updateReceiveStateAndRefreshUi(receiveState);
item.setOrder(0);
setSourceId(receiveState.getSourceId());
mSelectedPreference = item;
//refresh the header
if (mBluetoothFindBroadcastsHeaderController != null) {
mBluetoothFindBroadcastsHeaderController.refreshUi();
}
}
private BluetoothBroadcastSourcePreference createBluetoothBroadcastSourcePreference() {
BluetoothBroadcastSourcePreference pref = new BluetoothBroadcastSourcePreference(
getContext());
pref.setOnPreferenceClickListener(preference -> {
if (pref.getBluetoothLeBroadcastMetadata() == null) {
Log.d(TAG, "BluetoothLeBroadcastMetadata is null, do nothing.");
return false;
}
if (pref.isEncrypted()) {
launchBroadcastCodeDialog(pref);
} else {
addSource(pref);
}
return true;
});
return pref;
}
private void addSource(BluetoothBroadcastSourcePreference pref) {
if (mLeBroadcastAssistant == null || mCachedDevice == null) {
Log.w(TAG, "addSource: LeBroadcastAssistant or CachedDevice is null!");
return;
}
if (mSelectedPreference != null) {
// The previous preference status set false after user selects the new Preference.
getActivity().runOnUiThread(
() -> {
mSelectedPreference.updateMetadataAndRefreshUi(
mSelectedPreference.getBluetoothLeBroadcastMetadata(), false);
mSelectedPreference.setOrder(1);
});
}
mSelectedPreference = pref;
mLeBroadcastAssistant.addSource(mCachedDevice.getDevice(),
pref.getBluetoothLeBroadcastMetadata(), true);
}
private void addBroadcastCodeIntoPreference(BluetoothBroadcastSourcePreference pref,
String broadcastCode) {
BluetoothLeBroadcastMetadata metadata =
new BluetoothLeBroadcastMetadata.Builder(pref.getBluetoothLeBroadcastMetadata())
.setBroadcastCode(broadcastCode.getBytes(StandardCharsets.UTF_8))
.build();
pref.updateMetadataAndRefreshUi(metadata, false);
}
private void launchBroadcastCodeDialog(BluetoothBroadcastSourcePreference pref) {
final View layout = LayoutInflater.from(getContext()).inflate(
R.layout.bluetooth_find_broadcast_password_dialog, null);
final TextView broadcastName = layout.requireViewById(R.id.broadcast_name_text);
final EditText editText = layout.requireViewById(R.id.broadcast_edit_text);
broadcastName.setText(pref.getTitle());
AlertDialog alertDialog = new AlertDialog.Builder(getContext())
.setTitle(R.string.find_broadcast_password_dialog_title)
.setView(layout)
.setNeutralButton(android.R.string.cancel, null)
.setPositiveButton(R.string.bluetooth_connect_access_dialog_positive,
(d, w) -> {
Log.d(TAG, "setPositiveButton: clicked");
if (pref.getBluetoothLeBroadcastMetadata() == null) {
Log.d(TAG, "BluetoothLeBroadcastMetadata is null, do nothing.");
return;
}
addBroadcastCodeIntoPreference(pref, editText.getText().toString());
addSource(pref);
})
.create();
alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
alertDialog.show();
}
private void handleSearchStarted() {
cacheRemoveAllPrefs(mBroadcastSourceListCategory);
addConnectedSourcePreference();
}
private void handleSourceRemoved() {
if (mSelectedPreference != null) {
if (mSelectedPreference.getBluetoothLeBroadcastMetadata() == null) {
mBroadcastSourceListCategory.removePreference(mSelectedPreference);
} else {
mSelectedPreference.clearReceiveState();
}
}
mSelectedPreference = null;
}
private void addConnectedSourcePreference() {
List<BluetoothLeBroadcastReceiveState> receiveStateList =
mLeBroadcastAssistant.getAllSources(mCachedDevice.getDevice());
if (!receiveStateList.isEmpty()) {
updateListCategoryFromBroadcastReceiveState(receiveStateList.get(0));
}
}
public int getSourceId() {
return mSourceId;
}
public void setSourceId(int sourceId) {
mSourceId = sourceId;
}
}

View File

@@ -16,21 +16,22 @@
package com.android.settings.bluetooth;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import android.content.Intent;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.widget.LayoutPreference;
@@ -52,11 +53,12 @@ public class BluetoothFindBroadcastsHeaderController extends BluetoothDetailsCon
LinearLayout mBtnBroadcastLayout;
Button mBtnLeaveBroadcast;
Button mBtnScanQrCode;
BluetoothFindBroadcastsFragment mBluetoothFindBroadcastsFragment;
public BluetoothFindBroadcastsHeaderController(Context context,
PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle,
LocalBluetoothManager bluetoothManager) {
BluetoothFindBroadcastsFragment fragment, CachedBluetoothDevice device,
Lifecycle lifecycle, LocalBluetoothManager bluetoothManager) {
super(context, fragment, device, lifecycle);
mBluetoothFindBroadcastsFragment = fragment;
}
@Override
@@ -101,20 +103,41 @@ public class BluetoothFindBroadcastsHeaderController extends BluetoothDetailsCon
mBtnFindBroadcast.setVisibility(View.VISIBLE);
mBtnBroadcastLayout.setVisibility(View.GONE);
}
mBtnLeaveBroadcast.setEnabled(false);
if (mBluetoothFindBroadcastsFragment != null && mCachedDevice != null) {
LocalBluetoothLeBroadcastAssistant broadcastAssistant =
mBluetoothFindBroadcastsFragment.getLeBroadcastAssistant();
if (broadcastAssistant != null
&& broadcastAssistant.getConnectionStatus(mCachedDevice.getDevice())
== BluetoothProfile.STATE_CONNECTED) {
mBtnLeaveBroadcast.setEnabled(true);
}
}
}
private void scanBroadcastSource() {
// TODO(b/228258236) : Call the LocalBluetoothLeBroadcastAssistant
// to start searching for source
// TODO(b/231543455) : Using the BluetoothDeviceUpdater to refactor it.
if (mBluetoothFindBroadcastsFragment == null) {
return;
}
mBluetoothFindBroadcastsFragment.scanBroadcastSource();
}
private void leaveBroadcastSession() {
// TODO(b/228258236) : Call the LocalBluetoothLeBroadcastAssistant
// to leave the broadcast session
if (mBluetoothFindBroadcastsFragment == null) {
return;
}
mBluetoothFindBroadcastsFragment.leaveBroadcastSession();
}
private void launchQrCodeScanner() {
// TODO(b/228259065) : Launch the QR code scanner page by intent
final Intent intent = new Intent(mContext, QrCodeScanModeActivity.class);
intent.setAction(BluetoothBroadcastUtils.ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER)
.putExtra(BluetoothBroadcastUtils.EXTRA_BLUETOOTH_SINK_IS_GROUP, true)
.putExtra(BluetoothBroadcastUtils.EXTRA_BLUETOOTH_DEVICE_SINK,
mCachedDevice.getDevice());
mContext.startActivity(intent);
}
@Override
@@ -128,4 +151,11 @@ public class BluetoothFindBroadcastsHeaderController extends BluetoothDetailsCon
public String getPreferenceKey() {
return KEY_BROADCAST_HEADER;
}
/**
* Updates the UI
*/
public void refreshUi() {
updateHeaderLayout();
}
}

View File

@@ -147,15 +147,13 @@ public final class BluetoothPermissionRequest extends BroadcastReceiver {
title = context.getString(
R.string.bluetooth_sim_card_access_notification_title);
message = context.getString(
R.string.bluetooth_sim_card_access_notification_content,
deviceAlias, deviceAlias);
R.string.bluetooth_sim_card_access_notification_content);
break;
default:
title = context.getString(
R.string.bluetooth_connect_access_notification_title);
message = context.getString(
R.string.bluetooth_connect_access_notification_content,
deviceAlias, deviceAlias);
R.string.bluetooth_connect_access_notification_content);
break;
}
NotificationManager notificationManager =

View File

@@ -0,0 +1,88 @@
/*
* 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.bluetooth;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import com.android.settings.R;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.HearingAidProfile;
/**
* Provides a dialog to pair another side of hearing aid device.
*/
public class HearingAidPairingDialogFragment extends InstrumentedDialogFragment {
public static final String TAG = "HearingAidPairingDialogFragment";
private static final String KEY_CACHED_DEVICE_SIDE = "cached_device_side";
/**
* Creates a new {@link HearingAidPairingDialogFragment} and shows pair another side of hearing
* aid device according to {@code CachedBluetoothDevice} side.
*
* @param device The remote Bluetooth device, that needs to be hearing aid device.
* @return a DialogFragment
*/
public static HearingAidPairingDialogFragment newInstance(CachedBluetoothDevice device) {
Bundle args = new Bundle(1);
args.putInt(KEY_CACHED_DEVICE_SIDE, device.getDeviceSide());
final HearingAidPairingDialogFragment fragment = new HearingAidPairingDialogFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public int getMetricsCategory() {
// TODO(b/225117454): Need to update SettingsEnums later
return SettingsEnums.ACCESSIBILITY;
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final int deviceSide = getArguments().getInt(KEY_CACHED_DEVICE_SIDE);
final int titleId = R.string.bluetooth_pair_other_ear_dialog_title;
final int messageId = (deviceSide == HearingAidProfile.DeviceSide.SIDE_LEFT)
? R.string.bluetooth_pair_other_ear_dialog_left_ear_message
: R.string.bluetooth_pair_other_ear_dialog_right_ear_message;
final int pairBtnId = (deviceSide == HearingAidProfile.DeviceSide.SIDE_LEFT)
? R.string.bluetooth_pair_other_ear_dialog_right_ear_positive_button
: R.string.bluetooth_pair_other_ear_dialog_left_ear_positive_button;
return new AlertDialog.Builder(getActivity())
.setTitle(titleId)
.setMessage(messageId)
.setNegativeButton(
android.R.string.cancel, /* listener= */ null)
.setPositiveButton(pairBtnId, (dialog, which) -> positiveButtonListener())
.create();
}
private void positiveButtonListener() {
new SubSettingLauncher(getActivity())
.setDestination(BluetoothPairingDetail.class.getName())
.setSourceMetricsCategory(SettingsEnums.ACCESSIBILITY)
.launch();
}
}

View File

@@ -198,38 +198,25 @@ public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceContr
return drawable;
}
private int getBatteryTitleResource(int deviceId) {
if (deviceId == LEFT_DEVICE_ID) {
return R.id.bt_battery_left_title;
}
if (deviceId == RIGHT_DEVICE_ID) {
return R.id.bt_battery_right_title;
}
Log.d(TAG, "No resource id. The deviceId is " + deviceId);
return INVALID_RESOURCE_ID;
}
private int getBatterySummaryResource(int deviceId) {
if (deviceId == LEFT_DEVICE_ID) {
private int getBatterySummaryResource(int containerId) {
if (containerId == R.id.bt_battery_case) {
return R.id.bt_battery_case_summary;
} else if (containerId == R.id.bt_battery_left) {
return R.id.bt_battery_left_summary;
}
if (deviceId == RIGHT_DEVICE_ID) {
} else if (containerId == R.id.bt_battery_right) {
return R.id.bt_battery_right_summary;
}
Log.d(TAG, "No resource id. The deviceId is " + deviceId);
Log.d(TAG, "No summary resource id. The containerId is " + containerId);
return INVALID_RESOURCE_ID;
}
private void hideAllOfBatteryLayouts() {
// hide the case
updateBatteryLayout(R.id.bt_battery_case_title, R.id.bt_battery_case_summary,
BluetoothUtils.META_INT_ERROR);
updateBatteryLayout(R.id.bt_battery_case, BluetoothUtils.META_INT_ERROR);
// hide the left
updateBatteryLayout(R.id.bt_battery_left_title, R.id.bt_battery_left_summary,
BluetoothUtils.META_INT_ERROR);
updateBatteryLayout(R.id.bt_battery_left, BluetoothUtils.META_INT_ERROR);
// hide the right
updateBatteryLayout(R.id.bt_battery_right_title, R.id.bt_battery_right_summary,
BluetoothUtils.META_INT_ERROR);
updateBatteryLayout(R.id.bt_battery_right, BluetoothUtils.META_INT_ERROR);
}
private List<CachedBluetoothDevice> getAllOfLeAudioDevices() {
@@ -285,36 +272,36 @@ public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceContr
summary.setText(mCachedDevice.getConnectionSummary());
}
} else if (isLeft) {
updateBatteryLayout(getBatteryTitleResource(LEFT_DEVICE_ID),
getBatterySummaryResource(LEFT_DEVICE_ID), cachedDevice.getBatteryLevel());
updateBatteryLayout(R.id.bt_battery_left, cachedDevice.getBatteryLevel());
} else if (isRight) {
updateBatteryLayout(getBatteryTitleResource(RIGHT_DEVICE_ID),
getBatterySummaryResource(RIGHT_DEVICE_ID), cachedDevice.getBatteryLevel());
updateBatteryLayout(R.id.bt_battery_right, cachedDevice.getBatteryLevel());
} else {
Log.d(TAG, "The device id is other Audio Location. Do nothing.");
}
}
}
private void updateBatteryLayout(int titleResId, int summaryResId, int batteryLevel) {
final TextView batteryTitleView = mLayoutPreference.findViewById(titleResId);
final TextView batterySummaryView = mLayoutPreference.findViewById(summaryResId);
if (batteryTitleView == null || batterySummaryView == null) {
Log.e(TAG, "updateBatteryLayout: No TextView");
private void updateBatteryLayout(int resId, int batteryLevel) {
final View batteryView = mLayoutPreference.findViewById(resId);
if (batteryView == null) {
Log.e(TAG, "updateBatteryLayout: No View");
return;
}
if (batteryLevel != BluetoothUtils.META_INT_ERROR) {
batteryTitleView.setVisibility(View.VISIBLE);
batterySummaryView.setVisibility(View.VISIBLE);
batterySummaryView.setText(
com.android.settings.Utils.formatPercentage(batteryLevel));
batteryView.setVisibility(View.VISIBLE);
final TextView batterySummaryView =
batteryView.requireViewById(getBatterySummaryResource(resId));
final String batteryLevelPercentageString =
com.android.settings.Utils.formatPercentage(batteryLevel);
batterySummaryView.setText(batteryLevelPercentageString);
batterySummaryView.setContentDescription(mContext.getString(
R.string.bluetooth_battery_level, batteryLevelPercentageString));
batterySummaryView.setCompoundDrawablesRelativeWithIntrinsicBounds(
createBtBatteryIcon(mContext, batteryLevel), /* top */ null,
/* end */ null, /* bottom */ null);
} else {
Log.d(TAG, "updateBatteryLayout: Hide it if it doesn't have battery information.");
batteryTitleView.setVisibility(View.GONE);
batterySummaryView.setVisibility(View.GONE);
batteryView.setVisibility(View.GONE);
}
}

View File

@@ -0,0 +1,111 @@
/**
* 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.bluetooth;
import static com.android.settingslib.bluetooth.BluetoothBroadcastUtils.EXTRA_BLUETOOTH_DEVICE_SINK;
import static com.android.settingslib.bluetooth.BluetoothBroadcastUtils.EXTRA_BLUETOOTH_SINK_IS_GROUP;
import android.bluetooth.BluetoothDevice;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.fragment.app.FragmentTransaction;
import com.android.settingslib.R;
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
import com.android.settingslib.bluetooth.BluetoothUtils;
//TODO (b/232365943): Add test case for tthe QrCode UI.
public class QrCodeScanModeActivity extends QrCodeScanModeBaseActivity {
private static final boolean DEBUG = BluetoothUtils.D;
private static final String TAG = "QrCodeScanModeActivity";
private boolean mIsGroupOp;
private BluetoothDevice mSink;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void handleIntent(Intent intent) {
String action = intent != null ? intent.getAction() : null;
if (DEBUG) {
Log.d(TAG, "handleIntent(), action = " + action);
}
if (action == null) {
finish();
return;
}
switch (action) {
case BluetoothBroadcastUtils.ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER:
showQrCodeScannerFragment(intent);
break;
default:
if (DEBUG) {
Log.e(TAG, "Launch with an invalid action");
}
finish();
}
}
protected void showQrCodeScannerFragment(Intent intent) {
if (intent == null) {
if (DEBUG) {
Log.d(TAG, "intent is null, can not get bluetooth information from intent.");
}
return;
}
if (DEBUG) {
Log.d(TAG, "showQrCodeScannerFragment");
}
mSink = intent.getParcelableExtra(EXTRA_BLUETOOTH_DEVICE_SINK);
mIsGroupOp = intent.getBooleanExtra(EXTRA_BLUETOOTH_SINK_IS_GROUP, false);
if (DEBUG) {
Log.d(TAG, "get extra from intent");
}
QrCodeScanModeFragment fragment =
(QrCodeScanModeFragment) mFragmentManager.findFragmentByTag(
BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER);
if (fragment == null) {
fragment = new QrCodeScanModeFragment(mIsGroupOp, mSink);
} else {
if (fragment.isVisible()) {
return;
}
// When the fragment in back stack but not on top of the stack, we can simply pop
// stack because current fragment transactions are arranged in an order
mFragmentManager.popBackStackImmediate();
return;
}
final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fragment_container, fragment,
BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER);
fragmentTransaction.commit();
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.bluetooth;
import android.content.Intent;
import android.os.Bundle;
import androidx.fragment.app.FragmentManager;
import com.android.settingslib.R;
import com.android.settingslib.core.lifecycle.ObservableActivity;
public abstract class QrCodeScanModeBaseActivity extends ObservableActivity {
protected FragmentManager mFragmentManager;
protected abstract void handleIntent(Intent intent);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTheme(R.style.SudThemeGlifV3_DayNight);
setContentView(R.layout.qrcode_scan_mode_activity);
mFragmentManager = getSupportFragmentManager();
if (savedInstanceState == null) {
handleIntent(getIntent());
}
}
}

View File

@@ -0,0 +1,63 @@
/**
* 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.bluetooth;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.content.Context;
import android.util.Log;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastMetadata;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
public class QrCodeScanModeController {
private static final boolean DEBUG = BluetoothUtils.D;
private static final String TAG = "QrCodeScanModeController";
private LocalBluetoothLeBroadcastMetadata mLocalBroadcastMetadata;
private LocalBluetoothLeBroadcastAssistant mLocalBroadcastAssistant;
private LocalBluetoothManager mLocalBluetoothManager;
private LocalBluetoothProfileManager mProfileManager;
public QrCodeScanModeController(Context context) {
if (DEBUG) {
Log.d(TAG, "QrCodeScanModeController constructor.");
}
mLocalBluetoothManager = Utils.getLocalBtManager(context);
mProfileManager = mLocalBluetoothManager.getProfileManager();
mLocalBroadcastMetadata = new LocalBluetoothLeBroadcastMetadata();
CachedBluetoothDeviceManager cachedDeviceManager = new CachedBluetoothDeviceManager(context,
mLocalBluetoothManager);
mLocalBroadcastAssistant = new LocalBluetoothLeBroadcastAssistant(context,
cachedDeviceManager, mProfileManager);
}
private BluetoothLeBroadcastMetadata convertToBroadcastMetadata(String qrCodeString) {
return mLocalBroadcastMetadata.convertToBroadcastMetadata(qrCodeString);
}
public void addSource(BluetoothDevice sink, String sourceMetadata,
boolean isGroupOp) {
mLocalBroadcastAssistant.addSource(sink,
convertToBroadcastMetadata(sourceMetadata), isGroupOp);
}
}

View File

@@ -0,0 +1,243 @@
/**
* 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.bluetooth;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.util.Size;
import android.view.LayoutInflater;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.accessibility.AccessibilityEvent;
import android.widget.TextView;
import com.android.settings.core.InstrumentedFragment;
import com.android.settingslib.R;
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.core.lifecycle.ObservableFragment;
import com.android.settingslib.qrcode.QrCamera;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
public class QrCodeScanModeFragment extends InstrumentedFragment implements
TextureView.SurfaceTextureListener,
QrCamera.ScannerCallback {
private static final boolean DEBUG = BluetoothUtils.D;
private static final String TAG = "QrCodeScanModeFragment";
/** Message sent to hide error message */
private static final int MESSAGE_HIDE_ERROR_MESSAGE = 1;
/** Message sent to show error message */
private static final int MESSAGE_SHOW_ERROR_MESSAGE = 2;
/** Message sent to broadcast QR code */
private static final int MESSAGE_SCAN_BROADCAST_SUCCESS = 3;
private static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000;
private static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000;
private boolean mIsGroupOp;
private int mCornerRadius;
private BluetoothDevice mSink;
private String mBroadcastMetadata;
private Context mContext;
private QrCamera mCamera;
private QrCodeScanModeController mController;
private TextureView mTextureView;
private TextView mSummary;
private TextView mErrorMessage;
public QrCodeScanModeFragment(boolean isGroupOp, BluetoothDevice sink) {
mIsGroupOp = isGroupOp;
mSink = sink;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = getContext();
mController = new QrCodeScanModeController(mContext);
}
@Override
public final View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.qrcode_scanner_fragment, container,
/* attachToRoot */ false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mTextureView = view.findViewById(R.id.preview_view);
mCornerRadius = mContext.getResources().getDimensionPixelSize(
R.dimen.qrcode_preview_radius);
mTextureView.setSurfaceTextureListener(this);
mTextureView.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0,0, view.getWidth(), view.getHeight(), mCornerRadius);
}
});
mTextureView.setClipToOutline(true);
mErrorMessage = view.findViewById(R.id.error_message);
}
private void initCamera(SurfaceTexture surface) {
// Check if the camera has already created.
if (mCamera == null) {
mCamera = new QrCamera(mContext, this);
mCamera.start(surface);
}
}
private void destroyCamera() {
if (mCamera != null) {
mCamera.stop();
mCamera = null;
}
}
@Override
public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
initCamera(surface);
}
@Override
public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width,
int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
destroyCamera();
return true;
}
@Override
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {
}
@Override
public void handleSuccessfulResult(String qrCode) {
if (DEBUG) {
Log.d(TAG, "handleSuccessfulResult(), get the qr code string.");
}
mBroadcastMetadata = qrCode;
handleBtLeAudioScanner();
}
@Override
public void handleCameraFailure() {
destroyCamera();
}
@Override
public Size getViewSize() {
return new Size(mTextureView.getWidth(), mTextureView.getHeight());
}
@Override
public Rect getFramePosition(Size previewSize, int cameraOrientation) {
return new Rect(0, 0, previewSize.getHeight(), previewSize.getHeight());
}
@Override
public void setTransform(Matrix transform) {
mTextureView.setTransform(transform);
}
@Override
public boolean isValid(String qrCode) {
if (qrCode.startsWith(BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA)) {
return true;
} else {
showErrorMessage(R.string.bt_le_audio_qr_code_is_not_valid_format);
return false;
}
}
protected boolean isDecodeTaskAlive() {
return mCamera != null && mCamera.isDecodeTaskAlive();
}
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_HIDE_ERROR_MESSAGE:
mErrorMessage.setVisibility(View.INVISIBLE);
break;
case MESSAGE_SHOW_ERROR_MESSAGE:
final String errorMessage = (String) msg.obj;
mErrorMessage.setVisibility(View.VISIBLE);
mErrorMessage.setText(errorMessage);
mErrorMessage.sendAccessibilityEvent(
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
// Cancel any pending messages to hide error view and requeue the message so
// user has time to see error
removeMessages(MESSAGE_HIDE_ERROR_MESSAGE);
sendEmptyMessageDelayed(MESSAGE_HIDE_ERROR_MESSAGE,
SHOW_ERROR_MESSAGE_INTERVAL);
break;
case MESSAGE_SCAN_BROADCAST_SUCCESS:
mController.addSource(mSink, mBroadcastMetadata, mIsGroupOp);
updateSummary();
mSummary.sendAccessibilityEvent(
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
break;
default:
}
}
};
private void showErrorMessage(@StringRes int messageResId) {
final Message message = mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE,
getString(messageResId));
message.sendToTarget();
}
private void handleBtLeAudioScanner() {
Message message = mHandler.obtainMessage(MESSAGE_SCAN_BROADCAST_SUCCESS);
mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL);
}
private void updateSummary() {
mSummary.setText(getString(R.string.bt_le_audio_scan_qr_code_scanner,
null /* broadcast_name*/));;
}
@Override
public int getMetricsCategory() {
return SettingsEnums.LE_AUDIO_BROADCAST_SCAN_QR_CODE;
}
}

View File

@@ -16,10 +16,13 @@
package com.android.settings.bluetooth;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
import android.annotation.NonNull;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothStatusCodes;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
@@ -39,7 +42,7 @@ import androidx.appcompat.app.AlertDialog;
import com.android.settings.R;
import com.android.settingslib.bluetooth.BluetoothDiscoverableTimeoutReceiver;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
import java.time.Duration;
/**
* RequestPermissionActivity asks the user whether to enable discovery. This is
@@ -261,22 +264,26 @@ public class RequestPermissionActivity extends Activity implements
if (mRequest == REQUEST_ENABLE || mRequest == REQUEST_DISABLE) {
// BT toggled. Done
returnCode = RESULT_OK;
} else if (mBluetoothAdapter.setScanMode(
BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, mTimeout)) {
// If already in discoverable mode, this will extend the timeout.
long endTime = System.currentTimeMillis() + (long) mTimeout * 1000;
LocalBluetoothPreferences.persistDiscoverableEndTimestamp(
this, endTime);
if (0 < mTimeout) {
BluetoothDiscoverableTimeoutReceiver.setDiscoverableAlarm(this, endTime);
}
returnCode = mTimeout;
// Activity.RESULT_FIRST_USER should be 1
if (returnCode < RESULT_FIRST_USER) {
returnCode = RESULT_FIRST_USER;
}
} else {
returnCode = RESULT_CANCELED;
mBluetoothAdapter.setDiscoverableTimeout(Duration.ofSeconds(mTimeout));
if (mBluetoothAdapter.setScanMode(
BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)
== BluetoothStatusCodes.SUCCESS) {
// If already in discoverable mode, this will extend the timeout.
long endTime = System.currentTimeMillis() + (long) mTimeout * 1000;
LocalBluetoothPreferences.persistDiscoverableEndTimestamp(
this, endTime);
if (0 < mTimeout) {
BluetoothDiscoverableTimeoutReceiver.setDiscoverableAlarm(this, endTime);
}
returnCode = mTimeout;
// Activity.RESULT_FIRST_USER should be 1
if (returnCode < RESULT_FIRST_USER) {
returnCode = RESULT_FIRST_USER;
}
} else {
returnCode = RESULT_CANCELED;
}
}
if (mDialog != null) {

View File

@@ -15,6 +15,7 @@
*/
package com.android.settings.bluetooth;
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
@@ -117,6 +118,8 @@ public class SavedBluetoothDeviceUpdater extends BluetoothDeviceUpdater
if (device.isConnected()) {
return device.setActive();
}
mMetricsFeatureProvider.action(mPrefContext,
SettingsEnums.ACTION_SETTINGS_BLUETOOTH_CONNECT);
device.connect();
return true;
}