fix: 首次提交

This commit is contained in:
2024-12-09 11:25:23 +08:00
parent d0c01071e9
commit 2c2109a5f3
4741 changed files with 290641 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
/*
* Copyright (C) 2020 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.settingslib;
/**
* Content descriptions for accessibility support.
*/
public class AccessibilityContentDescriptions {
private AccessibilityContentDescriptions() {}
public static final int PHONE_SIGNAL_STRENGTH_NONE = R.string.accessibility_no_phone;
public static final int[] PHONE_SIGNAL_STRENGTH = {
PHONE_SIGNAL_STRENGTH_NONE,
R.string.accessibility_phone_one_bar,
R.string.accessibility_phone_two_bars,
R.string.accessibility_phone_three_bars,
R.string.accessibility_phone_signal_full
};
/**
* @param level int in range [0-4] that describes the signal level
* @return the appropriate content description for that signal strength, or 0 if the param is
* invalid
*/
public static int getDescriptionForLevel(int level) {
if (level > 4 || level < 0) {
return 0;
}
return PHONE_SIGNAL_STRENGTH[level];
}
public static final int[] PHONE_SIGNAL_STRENGTH_INFLATED = {
PHONE_SIGNAL_STRENGTH_NONE,
R.string.accessibility_phone_one_bar,
R.string.accessibility_phone_two_bars,
R.string.accessibility_phone_three_bars,
R.string.accessibility_phone_four_bars,
R.string.accessibility_phone_signal_full
};
/**
* @param level int in range [0-5] that describes the inflated signal level
* @return the appropriate content description for that signal strength, or 0 if the param is
* invalid
*/
public static int getDescriptionForInflatedLevel(int level) {
if (level > 5 || level < 0) {
return 0;
}
return PHONE_SIGNAL_STRENGTH_INFLATED[level];
}
/**
* @param level int in range [0-5] that describes the inflated signal level
* @param numberOfLevels one of (4, 5) that describes the default number of levels, or the
* inflated number of levels. The level param should be relative to the
* number of levels. This won't do any inflation.
* @return the appropriate content description for that signal strength, or 0 if the param is
* invalid
*/
public static int getDescriptionForLevel(int level, int numberOfLevels) {
if (numberOfLevels == 5) {
return getDescriptionForLevel(level);
} else if (numberOfLevels == 6) {
return getDescriptionForInflatedLevel(level);
} else {
return 0;
}
}
public static final int[] DATA_CONNECTION_STRENGTH = {
R.string.accessibility_no_data,
R.string.accessibility_data_one_bar,
R.string.accessibility_data_two_bars,
R.string.accessibility_data_three_bars,
R.string.accessibility_data_signal_full
};
public static final int[] WIFI_CONNECTION_STRENGTH = {
R.string.accessibility_no_wifi,
R.string.accessibility_wifi_one_bar,
R.string.accessibility_wifi_two_bars,
R.string.accessibility_wifi_three_bars,
R.string.accessibility_wifi_signal_full
};
public static final int WIFI_NO_CONNECTION = R.string.accessibility_no_wifi;
public static final int WIFI_OTHER_DEVICE_CONNECTION = R.string.accessibility_wifi_other_device;
public static final int NO_CALLING = R.string.accessibility_no_calling;
public static final int[] ETHERNET_CONNECTION_VALUES = {
R.string.accessibility_ethernet_disconnected,
R.string.accessibility_ethernet_connected,
};
}

View File

@@ -0,0 +1,85 @@
/*
* 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.settingslib;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseBooleanArray;
public class AppItem implements Comparable<AppItem>, Parcelable {
public static final int CATEGORY_USER = 0;
public static final int CATEGORY_APP_TITLE = 1;
public static final int CATEGORY_APP = 2;
public final int key;
public boolean restricted;
public int category;
public SparseBooleanArray uids = new SparseBooleanArray();
public long total;
public AppItem() {
this.key = 0;
}
public AppItem(int key) {
this.key = key;
}
public AppItem(Parcel parcel) {
key = parcel.readInt();
uids = parcel.readSparseBooleanArray();
total = parcel.readLong();
}
public void addUid(int uid) {
uids.put(uid, true);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(key);
dest.writeSparseBooleanArray(uids);
dest.writeLong(total);
}
@Override
public int describeContents() {
return 0;
}
@Override
public int compareTo(AppItem another) {
int comparison = Integer.compare(category, another.category);
if (comparison == 0) {
comparison = Long.compare(another.total, total);
}
return comparison;
}
public static final Creator<AppItem> CREATOR = new Creator<AppItem>() {
@Override
public AppItem createFromParcel(Parcel in) {
return new AppItem(in);
}
@Override
public AppItem[] newArray(int size) {
return new AppItem[size];
}
};
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.View;
import androidx.preference.DialogPreference;
import androidx.preference.PreferenceDialogFragment;
/**
* Framework version is deprecated, use the compat version instead.
*
* @deprecated
*/
@Deprecated
public class CustomDialogPreference extends DialogPreference {
private CustomPreferenceDialogFragment mFragment;
private DialogInterface.OnShowListener mOnShowListener;
public CustomDialogPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public CustomDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomDialogPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomDialogPreference(Context context) {
super(context);
}
public boolean isDialogOpen() {
return getDialog() != null && getDialog().isShowing();
}
public Dialog getDialog() {
return mFragment != null ? mFragment.getDialog() : null;
}
public void setOnShowListener(DialogInterface.OnShowListener listner) {
mOnShowListener = listner;
}
protected void onPrepareDialogBuilder(AlertDialog.Builder builder,
DialogInterface.OnClickListener listener) {
}
protected void onDialogClosed(boolean positiveResult) {
}
protected void onClick(DialogInterface dialog, int which) {
}
protected void onBindDialogView(View view) {
}
private void setFragment(CustomPreferenceDialogFragment fragment) {
mFragment = fragment;
}
private DialogInterface.OnShowListener getOnShowListener() {
return mOnShowListener;
}
public static class CustomPreferenceDialogFragment extends PreferenceDialogFragment {
public static CustomPreferenceDialogFragment newInstance(String key) {
final CustomPreferenceDialogFragment fragment = new CustomPreferenceDialogFragment();
final Bundle b = new Bundle(1);
b.putString(ARG_KEY, key);
fragment.setArguments(b);
return fragment;
}
private CustomDialogPreference getCustomizablePreference() {
return (CustomDialogPreference) getPreference();
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
getCustomizablePreference().setFragment(this);
getCustomizablePreference().onPrepareDialogBuilder(builder, this);
}
@Override
public void onDialogClosed(boolean positiveResult) {
getCustomizablePreference().onDialogClosed(positiveResult);
}
@Override
protected void onBindDialogView(View view) {
super.onBindDialogView(view);
getCustomizablePreference().onBindDialogView(view);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Dialog dialog = super.onCreateDialog(savedInstanceState);
dialog.setOnShowListener(getCustomizablePreference().getOnShowListener());
return dialog;
}
@Override
public void onClick(DialogInterface dialog, int which) {
super.onClick(dialog, which);
getCustomizablePreference().onClick(dialog, which);
}
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright (C) 2018 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.settingslib;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.View;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.DialogPreference;
import androidx.preference.PreferenceDialogFragmentCompat;
public class CustomDialogPreferenceCompat extends DialogPreference {
private CustomPreferenceDialogFragment mFragment;
private DialogInterface.OnShowListener mOnShowListener;
public CustomDialogPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public CustomDialogPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomDialogPreferenceCompat(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomDialogPreferenceCompat(Context context) {
super(context);
}
public boolean isDialogOpen() {
return getDialog() != null && getDialog().isShowing();
}
public Dialog getDialog() {
return mFragment != null ? mFragment.getDialog() : null;
}
public void setOnShowListener(DialogInterface.OnShowListener listner) {
mOnShowListener = listner;
}
protected void onPrepareDialogBuilder(AlertDialog.Builder builder,
DialogInterface.OnClickListener listener) {
}
protected void onDialogClosed(boolean positiveResult) {
}
protected void onClick(DialogInterface dialog, int which) {
}
protected void onBindDialogView(View view) {
}
private void setFragment(CustomPreferenceDialogFragment fragment) {
mFragment = fragment;
}
private DialogInterface.OnShowListener getOnShowListener() {
return mOnShowListener;
}
public static class CustomPreferenceDialogFragment extends PreferenceDialogFragmentCompat {
public static CustomPreferenceDialogFragment newInstance(String key) {
final CustomPreferenceDialogFragment fragment = new CustomPreferenceDialogFragment();
final Bundle b = new Bundle(1);
b.putString(ARG_KEY, key);
fragment.setArguments(b);
return fragment;
}
private CustomDialogPreferenceCompat getCustomizablePreference() {
return (CustomDialogPreferenceCompat) getPreference();
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
getCustomizablePreference().setFragment(this);
getCustomizablePreference().onPrepareDialogBuilder(builder, this);
}
@Override
public void onDialogClosed(boolean positiveResult) {
getCustomizablePreference().onDialogClosed(positiveResult);
}
@Override
protected void onBindDialogView(View view) {
super.onBindDialogView(view);
getCustomizablePreference().onBindDialogView(view);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Dialog dialog = super.onCreateDialog(savedInstanceState);
dialog.setOnShowListener(getCustomizablePreference().getOnShowListener());
return dialog;
}
@Override
public void onClick(DialogInterface dialog, int which) {
super.onClick(dialog, which);
getCustomizablePreference().onClick(dialog, which);
}
}
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib;
import static android.text.InputType.TYPE_CLASS_TEXT;
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.View;
import android.widget.EditText;
import androidx.annotation.CallSuper;
import androidx.preference.EditTextPreference;
import androidx.preference.EditTextPreferenceDialogFragment;
/**
* Framework version is deprecated, use the compat version instead.
*
* @deprecated
*/
@Deprecated
public class CustomEditTextPreference extends EditTextPreference {
private CustomPreferenceDialogFragment mFragment;
public CustomEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public CustomEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomEditTextPreference(Context context) {
super(context);
}
public EditText getEditText() {
if (mFragment != null) {
final Dialog dialog = mFragment.getDialog();
if (dialog != null) {
return (EditText) dialog.findViewById(android.R.id.edit);
}
}
return null;
}
public boolean isDialogOpen() {
return getDialog() != null && getDialog().isShowing();
}
public Dialog getDialog() {
return mFragment != null ? mFragment.getDialog() : null;
}
protected void onPrepareDialogBuilder(AlertDialog.Builder builder,
DialogInterface.OnClickListener listener) {
}
protected void onDialogClosed(boolean positiveResult) {
}
protected void onClick(DialogInterface dialog, int which) {
}
@CallSuper
protected void onBindDialogView(View view) {
final EditText editText = view.findViewById(android.R.id.edit);
if (editText != null) {
editText.setInputType(TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_CAP_SENTENCES);
editText.requestFocus();
}
}
private void setFragment(CustomPreferenceDialogFragment fragment) {
mFragment = fragment;
}
public static class CustomPreferenceDialogFragment extends EditTextPreferenceDialogFragment {
public static CustomPreferenceDialogFragment newInstance(String key) {
final CustomPreferenceDialogFragment fragment = new CustomPreferenceDialogFragment();
final Bundle b = new Bundle(1);
b.putString(ARG_KEY, key);
fragment.setArguments(b);
return fragment;
}
private CustomEditTextPreference getCustomizablePreference() {
return (CustomEditTextPreference) getPreference();
}
@Override
protected void onBindDialogView(View view) {
super.onBindDialogView(view);
getCustomizablePreference().onBindDialogView(view);
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
getCustomizablePreference().setFragment(this);
getCustomizablePreference().onPrepareDialogBuilder(builder, this);
}
@Override
public void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
getCustomizablePreference().onDialogClosed(positiveResult);
}
@Override
public void onClick(DialogInterface dialog, int which) {
super.onClick(dialog, which);
getCustomizablePreference().onClick(dialog, which);
}
}
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright (C) 2018 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.settingslib;
import static android.text.InputType.TYPE_CLASS_TEXT;
import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.View;
import android.widget.EditText;
import androidx.annotation.CallSuper;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.EditTextPreference;
import androidx.preference.EditTextPreferenceDialogFragmentCompat;
public class CustomEditTextPreferenceCompat extends EditTextPreference {
private CustomPreferenceDialogFragment mFragment;
public CustomEditTextPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public CustomEditTextPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomEditTextPreferenceCompat(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomEditTextPreferenceCompat(Context context) {
super(context);
}
public EditText getEditText() {
if (mFragment != null) {
final Dialog dialog = mFragment.getDialog();
if (dialog != null) {
return (EditText) dialog.findViewById(android.R.id.edit);
}
}
return null;
}
public boolean isDialogOpen() {
return getDialog() != null && getDialog().isShowing();
}
public Dialog getDialog() {
return mFragment != null ? mFragment.getDialog() : null;
}
protected void onPrepareDialogBuilder(AlertDialog.Builder builder,
DialogInterface.OnClickListener listener) {
}
protected void onDialogClosed(boolean positiveResult) {
}
protected void onClick(DialogInterface dialog, int which) {
}
@CallSuper
protected void onBindDialogView(View view) {
final EditText editText = view.findViewById(android.R.id.edit);
if (editText != null) {
editText.setInputType(TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_CAP_SENTENCES);
editText.requestFocus();
}
}
private void setFragment(CustomPreferenceDialogFragment fragment) {
mFragment = fragment;
}
public static class CustomPreferenceDialogFragment extends
EditTextPreferenceDialogFragmentCompat {
public static CustomPreferenceDialogFragment newInstance(String key) {
final CustomPreferenceDialogFragment fragment = new CustomPreferenceDialogFragment();
final Bundle b = new Bundle(1);
b.putString(ARG_KEY, key);
fragment.setArguments(b);
return fragment;
}
private CustomEditTextPreferenceCompat getCustomizablePreference() {
return (CustomEditTextPreferenceCompat) getPreference();
}
@Override
protected void onBindDialogView(View view) {
super.onBindDialogView(view);
getCustomizablePreference().onBindDialogView(view);
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
getCustomizablePreference().setFragment(this);
getCustomizablePreference().onPrepareDialogBuilder(builder, this);
}
@Override
public void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
getCustomizablePreference().onDialogClosed(positiveResult);
}
@Override
public void onClick(DialogInterface dialog, int which) {
super.onClick(dialog, which);
getCustomizablePreference().onClick(dialog, which);
}
}
}

View File

@@ -0,0 +1,222 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.settingslib;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.system.Os;
import android.system.StructUtsname;
import android.telephony.PhoneNumberUtils;
import android.telephony.SubscriptionInfo;
import android.telephony.TelephonyManager;
import android.text.BidiFormatter;
import android.text.TextDirectionHeuristics;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DeviceInfoUtils {
private static final String TAG = "DeviceInfoUtils";
private static final String FILENAME_MSV = "/sys/board_properties/soc/msv";
/**
* Reads a line from the specified file.
* @param filename the file to read from
* @return the first line, if any.
* @throws IOException if the file couldn't be read
*/
private static String readLine(String filename) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(filename), 256);
try {
return reader.readLine();
} finally {
reader.close();
}
}
public static String getFormattedKernelVersion(Context context) {
return formatKernelVersion(context, Os.uname());
}
@VisibleForTesting
static String formatKernelVersion(Context context, StructUtsname uname) {
if (uname == null) {
return context.getString(R.string.status_unavailable);
}
// Example:
// 4.9.29-g958411d
// #1 SMP PREEMPT Wed Jun 7 00:06:03 CST 2017
final String VERSION_REGEX =
"(#\\d+) " + /* group 1: "#1" */
"(?:.*?)?" + /* ignore: optional SMP, PREEMPT, and any CONFIG_FLAGS */
"((Sun|Mon|Tue|Wed|Thu|Fri|Sat).+)"; /* group 2: "Thu Jun 28 11:02:39 PDT 2012" */
Matcher m = Pattern.compile(VERSION_REGEX).matcher(uname.version);
if (!m.matches()) {
Log.e(TAG, "Regex did not match on uname version " + uname.version);
return context.getString(R.string.status_unavailable);
}
// Example output:
// 4.9.29-g958411d
// #1 Wed Jun 7 00:06:03 CST 2017
return new StringBuilder().append(uname.release)
.append("\n")
.append(m.group(1))
.append(" ")
.append(m.group(2)).toString();
}
/**
* Returns " (ENGINEERING)" if the msv file has a zero value, else returns "".
* @return a string to append to the model number description.
*/
public static String getMsvSuffix() {
// Production devices should have a non-zero value. If we can't read it, assume it's a
// production device so that we don't accidentally show that it's an ENGINEERING device.
try {
String msv = readLine(FILENAME_MSV);
// Parse as a hex number. If it evaluates to a zero, then it's an engineering build.
if (Long.parseLong(msv, 16) == 0) {
return " (ENGINEERING)";
}
} catch (IOException|NumberFormatException e) {
// Fail quietly, as the file may not exist on some devices, or may be unreadable
}
return "";
}
public static String getFeedbackReporterPackage(Context context) {
final String feedbackReporter =
context.getResources().getString(R.string.oem_preferred_feedback_reporter);
if (TextUtils.isEmpty(feedbackReporter)) {
// Reporter not configured. Return.
return feedbackReporter;
}
// Additional checks to ensure the reporter is on system image, and reporter is
// configured to listen to the intent. Otherwise, dont show the "send feedback" option.
final Intent intent = new Intent(Intent.ACTION_BUG_REPORT);
PackageManager pm = context.getPackageManager();
List<ResolveInfo> resolvedPackages =
pm.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER);
for (ResolveInfo info : resolvedPackages) {
if (info.activityInfo != null) {
if (!TextUtils.isEmpty(info.activityInfo.packageName)) {
try {
ApplicationInfo ai =
pm.getApplicationInfo(info.activityInfo.packageName, 0);
if ((ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
// Package is on the system image
if (TextUtils.equals(
info.activityInfo.packageName, feedbackReporter)) {
return feedbackReporter;
}
}
} catch (PackageManager.NameNotFoundException e) {
// No need to do anything here.
}
}
}
}
return null;
}
public static String getSecurityPatch() {
String patch = Build.VERSION.SECURITY_PATCH;
if (!"".equals(patch)) {
try {
SimpleDateFormat template = new SimpleDateFormat("yyyy-MM-dd");
Date patchDate = template.parse(patch);
String format = DateFormat.getBestDateTimePattern(Locale.getDefault(), "dMMMMyyyy");
patch = DateFormat.format(format, patchDate).toString();
} catch (ParseException e) {
// broken parse; fall through and use the raw string
}
return patch;
} else {
return null;
}
}
/**
* Format a phone number.
* @param subscriptionInfo {@link SubscriptionInfo} subscription information.
* @return Returns formatted phone number.
*/
public static String getFormattedPhoneNumber(Context context,
SubscriptionInfo subscriptionInfo) {
String formattedNumber = null;
if (subscriptionInfo != null) {
final TelephonyManager telephonyManager = context.getSystemService(
TelephonyManager.class);
final String rawNumber = telephonyManager.createForSubscriptionId(
subscriptionInfo.getSubscriptionId()).getLine1Number();
if (!TextUtils.isEmpty(rawNumber)) {
formattedNumber = PhoneNumberUtils.formatNumber(rawNumber);
}
}
return formattedNumber;
}
public static String getFormattedPhoneNumbers(Context context,
List<SubscriptionInfo> subscriptionInfoList) {
StringBuilder sb = new StringBuilder();
if (subscriptionInfoList != null) {
final TelephonyManager telephonyManager = context.getSystemService(
TelephonyManager.class);
final int count = subscriptionInfoList.size();
for (SubscriptionInfo subscriptionInfo : subscriptionInfoList) {
final String rawNumber = telephonyManager.createForSubscriptionId(
subscriptionInfo.getSubscriptionId()).getLine1Number();
if (!TextUtils.isEmpty(rawNumber)) {
sb.append(PhoneNumberUtils.formatNumber(rawNumber)).append("\n");
}
}
}
return sb.toString();
}
/**
* To get the formatting text for display in a potentially opposite-directionality context
* without garbling.
* @param subscriptionInfo {@link SubscriptionInfo} subscription information.
* @return Returns phone number with Bidi format.
*/
public static String getBidiFormattedPhoneNumber(Context context,
SubscriptionInfo subscriptionInfo) {
final String phoneNumber = getFormattedPhoneNumber(context, subscriptionInfo);
return BidiFormatter.getInstance().unicodeWrap(phoneNumber, TextDirectionHeuristics.LTR);
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.settingslib
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
/**
* A specification for the icon displaying the mobile network type -- 4G, 5G, LTE, etc. (aka "RAT
* icon" or "data type icon"). This is *not* the signal strength triangle.
*
* This is intended to eventually replace [SignalIcon.MobileIconGroup]. But for now,
* [MobileNetworkTypeIcons] just reads from the existing set of [SignalIcon.MobileIconGroup]
* instances to not duplicate data.
*
* TODO(b/238425913): Remove [SignalIcon.MobileIconGroup] and replace it with this class so that we
* don't need to fill in the superfluous fields from its parent [SignalIcon.IconGroup] class. Then
* this class can become either a sealed class or an enum with parameters.
*/
data class MobileNetworkTypeIcon(
/** A human-readable name for this network type, used for logging. */
val name: String,
/** The resource ID of the icon drawable to use. */
@DrawableRes val iconResId: Int,
/** The resource ID of the content description to use. */
@StringRes val contentDescriptionResId: Int,
)

View File

@@ -0,0 +1,67 @@
/*
* 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.settingslib
import com.android.settingslib.mobile.TelephonyIcons.ICON_NAME_TO_ICON
/**
* A utility class to fetch instances of [MobileNetworkTypeIcon] given a
* [SignalIcon.MobileIconGroup].
*
* Use [getNetworkTypeIcon] to fetch the instances.
*/
class MobileNetworkTypeIcons {
companion object {
/**
* A map from a [SignalIcon.MobileIconGroup.name] to an instance of [MobileNetworkTypeIcon],
* which is the preferred class going forward.
*/
private val MOBILE_NETWORK_TYPE_ICONS: Map<String, MobileNetworkTypeIcon>
init {
// Build up the mapping from the old implementation to the new one.
val tempMap: MutableMap<String, MobileNetworkTypeIcon> = mutableMapOf()
ICON_NAME_TO_ICON.forEach { (_, mobileIconGroup) ->
tempMap[mobileIconGroup.name] = mobileIconGroup.toNetworkTypeIcon()
}
MOBILE_NETWORK_TYPE_ICONS = tempMap
}
/**
* A converter function between the old mobile network type icon implementation and the new
* one. Given an instance of the old class [mobileIconGroup], outputs an instance of the
* new class [MobileNetworkTypeIcon].
*/
@JvmStatic
fun getNetworkTypeIcon(
mobileIconGroup: SignalIcon.MobileIconGroup
): MobileNetworkTypeIcon {
return MOBILE_NETWORK_TYPE_ICONS[mobileIconGroup.name]
?: mobileIconGroup.toNetworkTypeIcon()
}
private fun SignalIcon.MobileIconGroup.toNetworkTypeIcon(): MobileNetworkTypeIcon {
return MobileNetworkTypeIcon(
name = this.name,
iconResId = this.dataType,
contentDescriptionResId = this.dataContentDescription
)
}
}
}

View File

@@ -0,0 +1,208 @@
/*
* 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.settingslib;
import static android.net.NetworkPolicy.CYCLE_NONE;
import static android.net.NetworkPolicy.LIMIT_DISABLED;
import static android.net.NetworkPolicy.SNOOZE_NEVER;
import static android.net.NetworkPolicy.WARNING_DISABLED;
import static android.net.NetworkTemplate.MATCH_WIFI;
import static com.android.internal.util.Preconditions.checkNotNull;
import android.net.NetworkPolicy;
import android.net.NetworkPolicyManager;
import android.net.NetworkTemplate;
import android.os.AsyncTask;
import android.util.RecurrenceRule;
import com.google.android.collect.Lists;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
/**
* Utility class to modify list of {@link NetworkPolicy}. Specifically knows
* about which policies can coexist. This editor offers thread safety when
* talking with {@link NetworkPolicyManager}.
*
* @hide
*/
public class NetworkPolicyEditor {
// TODO: be more robust when missing policies from service
public static final boolean ENABLE_SPLIT_POLICIES = false;
private NetworkPolicyManager mPolicyManager;
private ArrayList<NetworkPolicy> mPolicies = Lists.newArrayList();
public NetworkPolicyEditor(NetworkPolicyManager policyManager) {
mPolicyManager = checkNotNull(policyManager);
}
public void read() {
final NetworkPolicy[] policies = mPolicyManager.getNetworkPolicies();
boolean modified = false;
mPolicies.clear();
for (NetworkPolicy policy : policies) {
// TODO: find better place to clamp these
if (policy.limitBytes < -1) {
policy.limitBytes = LIMIT_DISABLED;
modified = true;
}
if (policy.warningBytes < -1) {
policy.warningBytes = WARNING_DISABLED;
modified = true;
}
mPolicies.add(policy);
}
// when we cleaned policies above, write back changes
if (modified) writeAsync();
}
public void writeAsync() {
// TODO: consider making more robust by passing through service
final NetworkPolicy[] policies = mPolicies.toArray(new NetworkPolicy[mPolicies.size()]);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
write(policies);
return null;
}
}.execute();
}
public void write(NetworkPolicy[] policies) {
mPolicyManager.setNetworkPolicies(policies);
}
public boolean hasLimitedPolicy(NetworkTemplate template) {
final NetworkPolicy policy = getPolicy(template);
return policy != null && policy.limitBytes != LIMIT_DISABLED;
}
public NetworkPolicy getOrCreatePolicy(NetworkTemplate template) {
NetworkPolicy policy = getPolicy(template);
if (policy == null) {
policy = buildDefaultPolicy(template);
mPolicies.add(policy);
}
return policy;
}
public NetworkPolicy getPolicy(NetworkTemplate template) {
for (NetworkPolicy policy : mPolicies) {
if (policy.template.equals(template)) {
return policy;
}
}
return null;
}
public NetworkPolicy getPolicyMaybeUnquoted(NetworkTemplate template) {
NetworkPolicy policy = getPolicy(template);
if (policy != null) {
return policy;
} else {
return getPolicy(template);
}
}
@Deprecated
private static NetworkPolicy buildDefaultPolicy(NetworkTemplate template) {
// TODO: move this into framework to share with NetworkPolicyManagerService
final RecurrenceRule cycleRule;
final boolean metered;
if (template.getMatchRule() == MATCH_WIFI) {
cycleRule = RecurrenceRule.buildNever();
metered = false;
} else {
cycleRule = RecurrenceRule.buildRecurringMonthly(ZonedDateTime.now().getDayOfMonth(),
ZoneId.systemDefault());
metered = true;
}
return new NetworkPolicy(template, cycleRule, WARNING_DISABLED,
LIMIT_DISABLED, SNOOZE_NEVER, SNOOZE_NEVER, metered, true);
}
@Deprecated
public int getPolicyCycleDay(NetworkTemplate template) {
final NetworkPolicy policy = getPolicy(template);
if (policy != null && policy.cycleRule.isMonthly()) {
return policy.cycleRule.start.getDayOfMonth();
} else {
return CYCLE_NONE;
}
}
@Deprecated
public void setPolicyCycleDay(NetworkTemplate template, int cycleDay, String cycleTimezone) {
final NetworkPolicy policy = getOrCreatePolicy(template);
policy.cycleRule = NetworkPolicy.buildRule(cycleDay, ZoneId.of(cycleTimezone));
policy.inferred = false;
policy.clearSnooze();
writeAsync();
}
public long getPolicyWarningBytes(NetworkTemplate template) {
final NetworkPolicy policy = getPolicy(template);
return (policy != null) ? policy.warningBytes : WARNING_DISABLED;
}
private void setPolicyWarningBytesInner(NetworkTemplate template, long warningBytes) {
final NetworkPolicy policy = getOrCreatePolicy(template);
policy.warningBytes = warningBytes;
policy.inferred = false;
policy.clearSnooze();
writeAsync();
}
public void setPolicyWarningBytes(NetworkTemplate template, long warningBytes) {
long limitBytes = getPolicyLimitBytes(template);
warningBytes =
(limitBytes == LIMIT_DISABLED) ? warningBytes : Math.min(warningBytes, limitBytes);
setPolicyWarningBytesInner(template, warningBytes);
}
public long getPolicyLimitBytes(NetworkTemplate template) {
final NetworkPolicy policy = getPolicy(template);
return (policy != null) ? policy.limitBytes : LIMIT_DISABLED;
}
public void setPolicyLimitBytes(NetworkTemplate template, long limitBytes) {
long warningBytes = getPolicyWarningBytes(template);
if (warningBytes > limitBytes && limitBytes != LIMIT_DISABLED) {
setPolicyWarningBytesInner(template, limitBytes);
}
final NetworkPolicy policy = getOrCreatePolicy(template);
policy.limitBytes = limitBytes;
policy.inferred = false;
policy.clearSnooze();
writeAsync();
}
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright (C) 2021 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.settingslib;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import androidx.annotation.Keep;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceViewHolder;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.settingslib.core.instrumentation.SettingsJankMonitor;
/**
* A custom preference that provides inline switch toggle. It has a mandatory field for title, and
* optional fields for icon and sub-text. And it can be restricted by admin state.
*/
public class PrimarySwitchPreference extends RestrictedPreference {
private CompoundButton mSwitch;
private boolean mChecked;
private boolean mCheckedSet;
private boolean mEnableSwitch = true;
public PrimarySwitchPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public PrimarySwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public PrimarySwitchPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PrimarySwitchPreference(Context context) {
super(context);
}
@Override
protected int getSecondTargetResId() {
return androidx.preference.R.layout.preference_widget_switch_compat;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final View widgetFrame = holder.findViewById(android.R.id.widget_frame);
if (widgetFrame instanceof LinearLayout linearLayout) {
linearLayout.setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
}
mSwitch = (CompoundButton) holder.findViewById(androidx.preference.R.id.switchWidget);
if (mSwitch != null) {
mSwitch.setOnClickListener(v -> {
if (mSwitch != null && !mSwitch.isEnabled()) {
return;
}
final boolean newChecked = !mChecked;
if (callChangeListener(newChecked)) {
SettingsJankMonitor.detectToggleJank(getKey(), mSwitch);
setChecked(newChecked);
persistBoolean(newChecked);
}
});
// Consumes move events to ignore drag actions.
mSwitch.setOnTouchListener((v, event) -> {
return event.getActionMasked() == MotionEvent.ACTION_MOVE;
});
mSwitch.setContentDescription(getTitle());
mSwitch.setChecked(mChecked);
mSwitch.setEnabled(mEnableSwitch);
}
}
public boolean isChecked() {
return mSwitch != null && mChecked;
}
/**
* Used to validate the state of mChecked and mCheckedSet when testing, without requiring
* that a ViewHolder be bound to the object.
*/
@Keep
@Nullable
public Boolean getCheckedState() {
return mCheckedSet ? mChecked : null;
}
/**
* Set the checked status to be {@code checked}.
*
* @param checked The new checked status
*/
public void setChecked(boolean checked) {
// Always set checked the first time; don't assume the field's default of false.
final boolean changed = mChecked != checked;
if (changed || !mCheckedSet) {
mChecked = checked;
mCheckedSet = true;
if (mSwitch != null) {
mSwitch.setChecked(checked);
}
}
}
/**
* Set the Switch to be the status of {@code enabled}.
*
* @param enabled The new enabled status
*/
public void setSwitchEnabled(boolean enabled) {
mEnableSwitch = enabled;
if (mSwitch != null) {
mSwitch.setEnabled(enabled);
}
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public boolean isSwitchEnabled() {
return mEnableSwitch;
}
/**
* If admin is not null, disables the switch.
* Otherwise, keep it enabled.
*/
public void setDisabledByAdmin(EnforcedAdmin admin) {
super.setDisabledByAdmin(admin);
setSwitchEnabled(admin == null);
}
public CompoundButton getSwitch() {
return mSwitch;
}
@Override
protected boolean shouldHideSecondTarget() {
return getSecondTargetResId() == 0;
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2023 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.settingslib;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.preference.DropDownPreference;
import androidx.preference.PreferenceViewHolder;
public class RestrictedDropDownPreference extends DropDownPreference {
RestrictedPreferenceHelper mHelper;
public RestrictedDropDownPreference(@NonNull Context context) {
super(context);
mHelper = new RestrictedPreferenceHelper(context, this, null);
}
/**
* Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this
* package. Marks the preference as disabled if so.
* @param settingIdentifier The key identifying the setting
* @param packageName the package to check the settingIdentifier for
*/
public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
@NonNull String packageName) {
mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName);
}
@Override
public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
mHelper.onBindViewHolder(holder);
}
@Override
public void setEnabled(boolean enabled) {
if (enabled && isDisabledByEcm()) {
mHelper.setDisabledByEcm(null);
return;
}
super.setEnabled(enabled);
}
@Override
public void performClick() {
if (!mHelper.performClick()) {
super.performClick();
}
}
public boolean isDisabledByEcm() {
return mHelper.isDisabledByEcm();
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2016 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.settingslib;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.text.style.ImageSpan;
/**
* An extension of ImageSpan which adds a padding before the image.
*/
public class RestrictedLockImageSpan extends ImageSpan {
private Context mContext;
private final float mExtraPadding;
private final Drawable mRestrictedPadlock;
public RestrictedLockImageSpan(Context context) {
// we are overriding getDrawable, so passing null to super class here.
super((Drawable) null);
mContext = context;
mExtraPadding = mContext.getResources().getDimensionPixelSize(
R.dimen.restricted_icon_padding);
mRestrictedPadlock = RestrictedLockUtilsInternal.getRestrictedPadlock(mContext);
}
@Override
public Drawable getDrawable() {
return mRestrictedPadlock;
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y,
int bottom, Paint paint) {
Drawable drawable = getDrawable();
canvas.save();
// Add extra padding before the padlock.
float transX = x + mExtraPadding;
float transY = (bottom - drawable.getBounds().bottom) / 2.0f;
canvas.translate(transX, transY);
drawable.draw(canvas);
canvas.restore();
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end,
Paint.FontMetricsInt fontMetrics) {
int size = super.getSize(paint, text, start, end, fontMetrics);
size += 2 * mExtraPadding;
return size;
}
}

View File

@@ -0,0 +1,847 @@
/*
* Copyright (C) 2016 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.settingslib;
import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE;
import static android.app.admin.DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY;
import static android.app.admin.DevicePolicyManager.PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER;
import static android.app.role.RoleManager.ROLE_FINANCED_DEVICE_KIOSK;
import static com.android.settingslib.Utils.getColorAttrDefaultColor;
import android.annotation.UserIdInt;
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.admin.DevicePolicyManager;
import android.app.ecm.EnhancedConfirmationManager;
import android.app.role.RoleManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.UserManager.EnforcingUser;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.util.Log;
import android.view.MenuItem;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import com.android.internal.widget.LockPatternUtils;
import java.util.List;
/**
* Utility class to host methods usable in adding a restricted padlock icon and showing admin
* support message dialog.
*/
public class RestrictedLockUtilsInternal extends RestrictedLockUtils {
private static final String LOG_TAG = "RestrictedLockUtils";
private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
// TODO(b/281701062): reference role name from role manager once its exposed.
private static final String ROLE_DEVICE_LOCK_CONTROLLER =
"android.app.role.SYSTEM_FINANCED_DEVICE_CONTROLLER";
/**
* @return drawables for displaying with settings that are locked by a device admin.
*/
public static Drawable getRestrictedPadlock(Context context) {
Drawable restrictedPadlock = context.getDrawable(android.R.drawable.ic_info);
final int iconSize = context.getResources().getDimensionPixelSize(
android.R.dimen.config_restrictedIconSize);
TypedArray ta = context.obtainStyledAttributes(new int[]{android.R.attr.colorAccent});
int colorAccent = ta.getColor(0, 0);
ta.recycle();
restrictedPadlock.setTint(colorAccent);
restrictedPadlock.setBounds(0, 0, iconSize, iconSize);
return restrictedPadlock;
}
/**
* Checks if a given permission requires additional confirmation for the given package
*
* @return An intent to show the user if additional confirmation is required, null otherwise
*/
@Nullable
public static Intent checkIfRequiresEnhancedConfirmation(@NonNull Context context,
@NonNull String settingIdentifier, @NonNull String packageName) {
if (!android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()
|| !android.security.Flags.extendEcmToAllSettings()) {
return null;
}
EnhancedConfirmationManager ecManager = (EnhancedConfirmationManager) context
.getSystemService(Context.ECM_ENHANCED_CONFIRMATION_SERVICE);
try {
if (ecManager.isRestricted(packageName, settingIdentifier)) {
return ecManager.createRestrictedSettingDialogIntent(
packageName, settingIdentifier);
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(LOG_TAG, "package not found: " + packageName, e);
}
return null;
}
/**
* <p>This is {@code true} when the setting is a protected setting (i.e., a sensitive resource),
* and the app is restricted (i.e., considered dangerous), and the user has not yet cleared the
* app's restriction status (i.e., by clicking "Allow restricted settings" for this app). *
*/
public static boolean isEnhancedConfirmationRestricted(@NonNull Context context,
@NonNull String settingIdentifier, @NonNull String packageName) {
if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()
&& android.security.Flags.extendEcmToAllSettings()) {
try {
return context.getSystemService(EnhancedConfirmationManager.class)
.isRestricted(packageName, settingIdentifier);
} catch (PackageManager.NameNotFoundException e) {
Log.e(LOG_TAG, "Exception when retrieving package:" + packageName, e);
return false;
}
} else {
try {
if (!settingIdentifier.equals(AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE)) {
return false;
}
int uid = context.getPackageManager().getPackageUid(packageName, 0);
final int mode = context.getSystemService(AppOpsManager.class)
.noteOpNoThrow(AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS,
uid, packageName);
final boolean ecmEnabled = context.getResources().getBoolean(
com.android.internal.R.bool.config_enhancedConfirmationModeEnabled);
return ecmEnabled && mode != AppOpsManager.MODE_ALLOWED;
} catch (Exception e) {
// Fallback in case if app ops is not available in testing.
return false;
}
}
}
/**
* Checks if a restriction is enforced on a user and returns the enforced admin and
* admin userId.
*
* @param userRestriction Restriction to check
* @param userId User which we need to check if restriction is enforced on.
* @return EnforcedAdmin Object containing the enforced admin component and admin user details,
* or {@code null} If the restriction is not set. If the restriction is set by both device owner
* and profile owner, then the admin component will be set to {@code null} and userId to
* {@link UserHandle#USER_NULL}.
*/
public static EnforcedAdmin checkIfRestrictionEnforced(Context context,
String userRestriction, int userId) {
final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
if (dpm == null) {
return null;
}
final UserManager um = UserManager.get(context);
final UserHandle userHandle = UserHandle.of(userId);
final List<UserManager.EnforcingUser> enforcingUsers =
um.getUserRestrictionSources(userRestriction, userHandle);
if (enforcingUsers.isEmpty()) {
// Restriction is not enforced.
return null;
}
final int size = enforcingUsers.size();
if (size > 1) {
final EnforcedAdmin enforcedAdmin = EnforcedAdmin
.createDefaultEnforcedAdminWithRestriction(userRestriction);
enforcedAdmin.user = userHandle;
if (DEBUG) {
Log.d(LOG_TAG, "Multiple (" + size + ") enforcing users for restriction '"
+ userRestriction + "' on user " + userHandle + "; returning default admin "
+ "(" + enforcedAdmin + ")");
}
return enforcedAdmin;
}
final EnforcingUser enforcingUser = enforcingUsers.get(0);
final int restrictionSource = enforcingUser.getUserRestrictionSource();
if (restrictionSource == UserManager.RESTRICTION_SOURCE_SYSTEM) {
return null;
}
final EnforcedAdmin admin =
getProfileOrDeviceOwner(context, userRestriction, enforcingUser.getUserHandle());
if (admin != null) {
return admin;
}
return EnforcedAdmin.createDefaultEnforcedAdminWithRestriction(userRestriction);
}
public static boolean hasBaseUserRestriction(Context context,
String userRestriction, int userId) {
final UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
return um.hasBaseUserRestriction(userRestriction, UserHandle.of(userId));
}
/**
* Checks whether keyguard features are disabled by policy.
*
* @param context {@link Context} for the calling user.
*
* @param keyguardFeatures Any one of keyguard features that can be
* disabled by {@link android.app.admin.DevicePolicyManager#setKeyguardDisabledFeatures}.
*
* @param userId User to check enforced admin status for.
*
* @return EnforcedAdmin Object containing the enforced admin component and admin user details,
* or {@code null} If the notification features are not disabled. If the restriction is set by
* multiple admins, then the admin component will be set to {@code null} and userId to
* {@link UserHandle#USER_NULL}.
*/
public static EnforcedAdmin checkIfKeyguardFeaturesDisabled(Context context,
int keyguardFeatures, final @UserIdInt int userId) {
final LockSettingCheck check = (dpm, admin, checkUser) -> {
int effectiveFeatures = dpm.getKeyguardDisabledFeatures(admin, checkUser);
if (checkUser != userId) {
effectiveFeatures &= PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER;
}
return (effectiveFeatures & keyguardFeatures) != KEYGUARD_DISABLE_FEATURES_NONE;
};
if (UserManager.get(context).getUserInfo(userId).isManagedProfile()) {
DevicePolicyManager dpm =
(DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
return findEnforcedAdmin(dpm.getActiveAdminsAsUser(userId), dpm, userId, check);
}
return checkForLockSetting(context, userId, check);
}
/**
* @return the UserHandle for a userId. Return null for USER_NULL
*/
private static UserHandle getUserHandleOf(@UserIdInt int userId) {
if (userId == UserHandle.USER_NULL) {
return null;
} else {
return UserHandle.of(userId);
}
}
/**
* Filter a set of device admins based on a predicate {@code check}. This is equivalent to
* {@code admins.stream().filter(check).map(x → new EnforcedAdmin(admin, userId)} except it's
* returning a zero/one/many-type thing.
*
* @param admins set of candidate device admins identified by {@link ComponentName}.
* @param userId user to create the resultant {@link EnforcedAdmin} as.
* @param check filter predicate.
*
* @return {@code null} if none of the {@param admins} match.
* An {@link EnforcedAdmin} if exactly one of the admins matches.
* Otherwise, {@link EnforcedAdmin#MULTIPLE_ENFORCED_ADMIN} for multiple matches.
*/
@Nullable
private static EnforcedAdmin findEnforcedAdmin(@Nullable List<ComponentName> admins,
@NonNull DevicePolicyManager dpm, @UserIdInt int userId,
@NonNull LockSettingCheck check) {
if (admins == null) {
return null;
}
final UserHandle user = getUserHandleOf(userId);
EnforcedAdmin enforcedAdmin = null;
for (ComponentName admin : admins) {
if (check.isEnforcing(dpm, admin, userId)) {
if (enforcedAdmin == null) {
enforcedAdmin = new EnforcedAdmin(admin, user);
} else {
return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
}
}
}
return enforcedAdmin;
}
public static EnforcedAdmin checkIfUninstallBlocked(Context context,
String packageName, int userId) {
EnforcedAdmin allAppsControlDisallowedAdmin = checkIfRestrictionEnforced(context,
UserManager.DISALLOW_APPS_CONTROL, userId);
if (allAppsControlDisallowedAdmin != null) {
return allAppsControlDisallowedAdmin;
}
EnforcedAdmin allAppsUninstallDisallowedAdmin = checkIfRestrictionEnforced(context,
UserManager.DISALLOW_UNINSTALL_APPS, userId);
if (allAppsUninstallDisallowedAdmin != null) {
return allAppsUninstallDisallowedAdmin;
}
IPackageManager ipm = AppGlobals.getPackageManager();
try {
if (ipm.getBlockUninstallForUser(packageName, userId)) {
return getProfileOrDeviceOwner(context, getUserHandleOf(userId));
}
} catch (RemoteException e) {
// Nothing to do
}
return null;
}
/**
* Check if an application is suspended.
*
* @return EnforcedAdmin Object containing the enforced admin component and admin user details,
* or {@code null} if the application is not suspended.
*/
public static EnforcedAdmin checkIfApplicationIsSuspended(Context context, String packageName,
int userId) {
IPackageManager ipm = AppGlobals.getPackageManager();
try {
if (ipm.isPackageSuspendedForUser(packageName, userId)) {
return getProfileOrDeviceOwner(context, getUserHandleOf(userId));
}
} catch (RemoteException | IllegalArgumentException e) {
// Nothing to do
}
return null;
}
public static EnforcedAdmin checkIfInputMethodDisallowed(Context context,
String packageName, int userId) {
DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
if (dpm == null) {
return null;
}
EnforcedAdmin admin = getProfileOrDeviceOwner(context, getUserHandleOf(userId));
boolean permitted = true;
if (admin != null) {
permitted = dpm.isInputMethodPermittedByAdmin(admin.component,
packageName, userId);
}
boolean permittedByParentAdmin = true;
EnforcedAdmin profileAdmin = null;
int managedProfileId = getManagedProfileId(context, userId);
if (managedProfileId != UserHandle.USER_NULL) {
profileAdmin = getProfileOrDeviceOwner(context, getUserHandleOf(managedProfileId));
// If the device is an organization-owned device with a managed profile, the
// managedProfileId will be used instead of the affected userId. This is because
// isInputMethodPermittedByAdmin is called on the parent DPM instance, which will
// return results affecting the personal profile.
if (profileAdmin != null && dpm.isOrganizationOwnedDeviceWithManagedProfile()) {
DevicePolicyManager parentDpm = sProxy.getParentProfileInstance(dpm,
UserManager.get(context).getUserInfo(managedProfileId));
permittedByParentAdmin = parentDpm.isInputMethodPermittedByAdmin(
profileAdmin.component, packageName, managedProfileId);
}
}
if (!permitted && !permittedByParentAdmin) {
return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
} else if (!permitted) {
return admin;
} else if (!permittedByParentAdmin) {
return profileAdmin;
}
return null;
}
/**
* @param context
* @param userId user id of a managed profile.
* @return is remote contacts search disallowed.
*/
public static EnforcedAdmin checkIfRemoteContactSearchDisallowed(Context context, int userId) {
DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
if (dpm == null) {
return null;
}
EnforcedAdmin admin = getProfileOwner(context, userId);
if (admin == null) {
return null;
}
UserHandle userHandle = UserHandle.of(userId);
if (dpm.getCrossProfileContactsSearchDisabled(userHandle)
&& dpm.getCrossProfileCallerIdDisabled(userHandle)) {
return admin;
}
return null;
}
public static EnforcedAdmin checkIfAccessibilityServiceDisallowed(Context context,
String packageName, int userId) {
DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
if (dpm == null) {
return null;
}
EnforcedAdmin admin = getProfileOrDeviceOwner(context, getUserHandleOf(userId));
boolean permitted = true;
if (admin != null) {
permitted = dpm.isAccessibilityServicePermittedByAdmin(admin.component,
packageName, userId);
}
int managedProfileId = getManagedProfileId(context, userId);
EnforcedAdmin profileAdmin = getProfileOrDeviceOwner(context,
getUserHandleOf(managedProfileId));
boolean permittedByProfileAdmin = true;
if (profileAdmin != null) {
permittedByProfileAdmin = dpm.isAccessibilityServicePermittedByAdmin(
profileAdmin.component, packageName, managedProfileId);
}
if (!permitted && !permittedByProfileAdmin) {
return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
} else if (!permitted) {
return admin;
} else if (!permittedByProfileAdmin) {
return profileAdmin;
}
return null;
}
private static int getManagedProfileId(Context context, int userId) {
UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
List<UserInfo> userProfiles = um.getProfiles(userId);
for (UserInfo uInfo : userProfiles) {
if (uInfo.id == userId) {
continue;
}
if (uInfo.isManagedProfile()) {
return uInfo.id;
}
}
return UserHandle.USER_NULL;
}
/**
* Check if account management for a specific type of account is disabled by admin.
* Only a profile or device owner can disable account management. So, we check if account
* management is disabled and return profile or device owner on the calling user.
*
* @return EnforcedAdmin Object containing the enforced admin component and admin user details,
* or {@code null} if the account management is not disabled.
*/
public static EnforcedAdmin checkIfAccountManagementDisabled(Context context,
String accountType, int userId) {
if (accountType == null) {
return null;
}
DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
PackageManager pm = context.getPackageManager();
if (!pm.hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN) || dpm == null) {
return null;
}
boolean isAccountTypeDisabled = false;
String[] disabledTypes = dpm.getAccountTypesWithManagementDisabledAsUser(userId);
for (String type : disabledTypes) {
if (accountType.equals(type)) {
isAccountTypeDisabled = true;
break;
}
}
if (!isAccountTypeDisabled) {
return null;
}
return getProfileOrDeviceOwner(context, getUserHandleOf(userId));
}
/**
* Check if USB data signaling (except from charging functions) is disabled by the admin.
* Only a device owner or a profile owner on an organization-owned managed profile can disable
* USB data signaling.
*
* @return EnforcedAdmin Object containing the enforced admin component and admin user details,
* or {@code null} if USB data signaling is not disabled.
*/
public static EnforcedAdmin checkIfUsbDataSignalingIsDisabled(Context context, int userId) {
DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
if (dpm == null || dpm.isUsbDataSignalingEnabled()) {
return null;
} else {
EnforcedAdmin admin = getProfileOrDeviceOwner(context, getUserHandleOf(userId));
int managedProfileId = getManagedProfileId(context, userId);
if (admin == null && managedProfileId != UserHandle.USER_NULL) {
admin = getProfileOrDeviceOwner(context, getUserHandleOf(managedProfileId));
}
return admin;
}
}
/**
* Check if user control over metered data usage of {@code packageName} is disabled by the
* profile or device owner.
*
* @return EnforcedAdmin object containing the enforced admin component and admin user details,
* or {@code null} if the user control is not disabled.
*/
public static EnforcedAdmin checkIfMeteredDataUsageUserControlDisabled(Context context,
String packageName, int userId) {
RoleManager roleManager = context.getSystemService(RoleManager.class);
UserHandle userHandle = getUserHandleOf(userId);
if (roleManager.getRoleHoldersAsUser(ROLE_FINANCED_DEVICE_KIOSK, userHandle)
.contains(packageName)
|| roleManager.getRoleHoldersAsUser(ROLE_DEVICE_LOCK_CONTROLLER, userHandle)
.contains(packageName)) {
// There is no actual device admin for a financed device, but metered data usage
// control should still be disabled for both controller and kiosk apps.
return new EnforcedAdmin();
}
final EnforcedAdmin enforcedAdmin = getProfileOrDeviceOwner(context,
userHandle);
if (enforcedAdmin == null) {
return null;
}
final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
return dpm.isMeteredDataDisabledPackageForUser(enforcedAdmin.component, packageName, userId)
? enforcedAdmin : null;
}
/**
* Checks if an admin has enforced minimum password quality or complexity requirements on the
* given user.
*
* @return EnforcedAdmin Object containing the enforced admin component and admin user details,
* or {@code null} if no quality requirements are set. If the requirements are set by
* multiple device admins, then the admin component will be set to {@code null} and userId to
* {@link UserHandle#USER_NULL}.
*/
public static EnforcedAdmin checkIfPasswordQualityIsSet(Context context, int userId) {
final LockSettingCheck check =
(DevicePolicyManager dpm, ComponentName admin, @UserIdInt int checkUser) ->
dpm.getPasswordQuality(admin, checkUser)
> DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
if (dpm == null) {
return null;
}
LockPatternUtils lockPatternUtils = new LockPatternUtils(context);
final int aggregatedComplexity = dpm.getAggregatedPasswordComplexityForUser(userId);
if (aggregatedComplexity > DevicePolicyManager.PASSWORD_COMPLEXITY_NONE) {
// First, check if there's a Device Owner. If so, then only it can apply password
// complexity requiremnts (there can be no secondary profiles).
final UserHandle deviceOwnerUser = dpm.getDeviceOwnerUser();
if (deviceOwnerUser != null) {
return new EnforcedAdmin(dpm.getDeviceOwnerComponentOnAnyUser(), deviceOwnerUser);
}
// The complexity could be enforced by a Profile Owner - either in the current user
// or the current user is the parent user that is affected by the profile owner.
for (UserInfo userInfo : UserManager.get(context).getProfiles(userId)) {
final ComponentName profileOwnerComponent = dpm.getProfileOwnerAsUser(userInfo.id);
if (profileOwnerComponent != null) {
return new EnforcedAdmin(profileOwnerComponent, getUserHandleOf(userInfo.id));
}
}
// Should not get here: A Device Owner or Profile Owner should be found.
throw new IllegalStateException(
String.format("Could not find admin enforcing complexity %d for user %d",
aggregatedComplexity, userId));
}
if (sProxy.isSeparateProfileChallengeEnabled(lockPatternUtils, userId)) {
// userId is managed profile and has a separate challenge, only consider
// the admins in that user.
final List<ComponentName> admins = dpm.getActiveAdminsAsUser(userId);
if (admins == null) {
return null;
}
EnforcedAdmin enforcedAdmin = null;
final UserHandle user = getUserHandleOf(userId);
for (ComponentName admin : admins) {
if (check.isEnforcing(dpm, admin, userId)) {
if (enforcedAdmin == null) {
enforcedAdmin = new EnforcedAdmin(admin, user);
} else {
return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
}
}
}
return enforcedAdmin;
} else {
return checkForLockSetting(context, userId, check);
}
}
/**
* Checks if any admin has set maximum time to lock.
*
* @return EnforcedAdmin Object containing the enforced admin component and admin user details,
* or {@code null} if no admin has set this restriction. If multiple admins has set this, then
* the admin component will be set to {@code null} and userId to {@link UserHandle#USER_NULL}
*/
public static EnforcedAdmin checkIfMaximumTimeToLockIsSet(Context context) {
return checkForLockSetting(context, UserHandle.myUserId(),
(DevicePolicyManager dpm, ComponentName admin, @UserIdInt int userId) ->
dpm.getMaximumTimeToLock(admin, userId) > 0);
}
private interface LockSettingCheck {
boolean isEnforcing(DevicePolicyManager dpm, ComponentName admin, @UserIdInt int userId);
}
/**
* Checks whether any of the user's profiles enforce the lock setting. A managed profile is only
* included if it does not have a separate challenge.
*
* The user identified by {@param userId} is always included.
*/
private static EnforcedAdmin checkForLockSetting(
Context context, @UserIdInt int userId, LockSettingCheck check) {
final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
if (dpm == null) {
return null;
}
final LockPatternUtils lockPatternUtils = new LockPatternUtils(context);
EnforcedAdmin enforcedAdmin = null;
// Return all admins for this user and the profiles that are visible from this
// user that do not use a separate work challenge.
for (UserInfo userInfo : UserManager.get(context).getProfiles(userId)) {
final List<ComponentName> admins = dpm.getActiveAdminsAsUser(userInfo.id);
if (admins == null) {
continue;
}
final UserHandle user = getUserHandleOf(userInfo.id);
final boolean isSeparateProfileChallengeEnabled =
sProxy.isSeparateProfileChallengeEnabled(lockPatternUtils, userInfo.id);
for (ComponentName admin : admins) {
if (!isSeparateProfileChallengeEnabled) {
if (check.isEnforcing(dpm, admin, userInfo.id)) {
if (enforcedAdmin == null) {
enforcedAdmin = new EnforcedAdmin(admin, user);
} else {
return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
}
// This same admins could have set policies both on the managed profile
// and on the parent. So, if the admin has set the policy on the
// managed profile here, we don't need to further check if that admin
// has set policy on the parent admin.
continue;
}
}
if (userInfo.isManagedProfile()) {
// If userInfo.id is a managed profile, we also need to look at
// the policies set on the parent.
DevicePolicyManager parentDpm = sProxy.getParentProfileInstance(dpm, userInfo);
if (check.isEnforcing(parentDpm, admin, userInfo.id)) {
if (enforcedAdmin == null) {
enforcedAdmin = new EnforcedAdmin(admin, user);
} else {
return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
}
}
}
}
}
return enforcedAdmin;
}
public static EnforcedAdmin getDeviceOwner(Context context) {
return getDeviceOwner(context, null);
}
private static EnforcedAdmin getDeviceOwner(Context context, String enforcedRestriction) {
final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
if (dpm == null) {
return null;
}
ComponentName adminComponent = dpm.getDeviceOwnerComponentOnAnyUser();
if (adminComponent != null) {
return new EnforcedAdmin(
adminComponent, enforcedRestriction, dpm.getDeviceOwnerUser());
}
return null;
}
private static EnforcedAdmin getProfileOwner(Context context, int userId) {
return getProfileOwner(context, null, userId);
}
private static EnforcedAdmin getProfileOwner(
Context context, String enforcedRestriction, int userId) {
if (userId == UserHandle.USER_NULL) {
return null;
}
final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
if (dpm == null) {
return null;
}
ComponentName adminComponent = dpm.getProfileOwnerAsUser(userId);
if (adminComponent != null) {
return new EnforcedAdmin(adminComponent, enforcedRestriction, getUserHandleOf(userId));
}
return null;
}
/**
* Set the menu item as disabled by admin by adding a restricted padlock at the end of the
* text and set the click listener which will send an intent to show the admin support details
* dialog. If the admin is null, remove the padlock and disabled color span. When the admin is
* null, we also set the OnMenuItemClickListener to null, so if you want to set a custom
* OnMenuItemClickListener, set it after calling this method.
*/
public static void setMenuItemAsDisabledByAdmin(final Context context,
final MenuItem item, final EnforcedAdmin admin) {
SpannableStringBuilder sb = new SpannableStringBuilder(item.getTitle());
removeExistingRestrictedSpans(sb);
if (admin != null) {
final int disabledColor = getColorAttrDefaultColor(context,
android.R.attr.textColorHint);
sb.setSpan(new ForegroundColorSpan(disabledColor), 0, sb.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ImageSpan image = new RestrictedLockImageSpan(context);
sb.append(" ", image, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
sendShowAdminSupportDetailsIntent(context, admin);
return true;
}
});
} else {
item.setOnMenuItemClickListener(null);
}
item.setTitle(sb);
}
private static void removeExistingRestrictedSpans(SpannableStringBuilder sb) {
final int length = sb.length();
RestrictedLockImageSpan[] imageSpans = sb.getSpans(length - 1, length,
RestrictedLockImageSpan.class);
for (ImageSpan span : imageSpans) {
final int start = sb.getSpanStart(span);
final int end = sb.getSpanEnd(span);
sb.removeSpan(span);
sb.delete(start, end);
}
ForegroundColorSpan[] colorSpans = sb.getSpans(0, length, ForegroundColorSpan.class);
for (ForegroundColorSpan span : colorSpans) {
sb.removeSpan(span);
}
}
public static boolean isAdminInCurrentUserOrProfile(Context context, ComponentName admin) {
DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService(
Context.DEVICE_POLICY_SERVICE);
UserManager um = UserManager.get(context);
for (UserInfo userInfo : um.getProfiles(UserHandle.myUserId())) {
if (dpm.isAdminActiveAsUser(admin, userInfo.id)) {
return true;
}
}
return false;
}
public static void setTextViewPadlock(Context context,
TextView textView, boolean showPadlock) {
final SpannableStringBuilder sb = new SpannableStringBuilder(textView.getText());
removeExistingRestrictedSpans(sb);
if (showPadlock) {
final ImageSpan image = new RestrictedLockImageSpan(context);
sb.append(" ", image, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
textView.setText(sb);
}
/**
* Takes a {@link android.widget.TextView} and applies an alpha so that the text looks like
* disabled and appends a padlock to the text. This assumes that there are no
* ForegroundColorSpans and RestrictedLockImageSpans used on the TextView.
*/
public static void setTextViewAsDisabledByAdmin(Context context,
TextView textView, boolean disabled) {
final SpannableStringBuilder sb = new SpannableStringBuilder(textView.getText());
removeExistingRestrictedSpans(sb);
if (disabled) {
final int disabledColor = Utils.getDisabled(context,
textView.getCurrentTextColor());
sb.setSpan(new ForegroundColorSpan(disabledColor), 0, sb.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setCompoundDrawables(null, null, getRestrictedPadlock(context), null);
textView.setCompoundDrawablePadding(context.getResources().getDimensionPixelSize(
R.dimen.restricted_icon_padding));
} else {
textView.setCompoundDrawables(null, null, null, null);
}
textView.setText(sb);
}
/**
* Checks whether MTE (Advanced memory protection) controls are disabled by the enterprise
* policy.
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public static EnforcedAdmin checkIfMteIsDisabled(Context context) {
final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
if (dpm.getMtePolicy() == MTE_NOT_CONTROLLED_BY_POLICY) {
return null;
}
EnforcedAdmin admin =
RestrictedLockUtils.getProfileOrDeviceOwner(
context, UserHandle.of(UserHandle.USER_SYSTEM));
if (admin != null) {
return admin;
}
int profileId = getManagedProfileId(context, UserHandle.USER_SYSTEM);
return RestrictedLockUtils.getProfileOrDeviceOwner(context, UserHandle.of(profileId));
}
/**
* Static {@link LockPatternUtils} and {@link DevicePolicyManager} wrapper for testing purposes.
* {@link LockPatternUtils} is an internal API not supported by robolectric.
* {@link DevicePolicyManager} has a {@code getProfileParent} not yet suppored by robolectric.
*/
@VisibleForTesting
static Proxy sProxy = new Proxy();
@VisibleForTesting
static class Proxy {
public boolean isSeparateProfileChallengeEnabled(LockPatternUtils utils, int userHandle) {
return utils.isSeparateProfileChallengeEnabled(userHandle);
}
public DevicePolicyManager getParentProfileInstance(DevicePolicyManager dpm, UserInfo ui) {
return dpm.getParentProfileInstance(ui);
}
}
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright (C) 2016 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.settingslib;
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import android.content.Context;
import android.os.Process;
import android.os.UserHandle;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.core.content.res.TypedArrayUtils;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceViewHolder;
import com.android.settingslib.widget.TwoTargetPreference;
/**
* Preference class that supports being disabled by a user restriction
* set by a device admin.
*/
public class RestrictedPreference extends TwoTargetPreference {
RestrictedPreferenceHelper mHelper;
public RestrictedPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes, String packageName, int uid) {
super(context, attrs, defStyleAttr, defStyleRes);
mHelper = new RestrictedPreferenceHelper(context, this, attrs, packageName, uid);
}
public RestrictedPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
this(context, attrs, defStyleAttr, defStyleRes, null, Process.INVALID_UID);
}
public RestrictedPreference(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public RestrictedPreference(Context context, AttributeSet attrs) {
this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.preferenceStyle,
android.R.attr.preferenceStyle));
}
public RestrictedPreference(Context context) {
this(context, null);
}
public RestrictedPreference(Context context, String packageName, int uid) {
this(context, null, TypedArrayUtils.getAttr(context, R.attr.preferenceStyle,
android.R.attr.preferenceStyle), 0, packageName, uid);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
mHelper.onBindViewHolder(holder);
}
@Override
public void performClick() {
if (!mHelper.performClick()) {
super.performClick();
}
}
public void useAdminDisabledSummary(boolean useSummary) {
mHelper.useAdminDisabledSummary(useSummary);
}
@Override
protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
mHelper.onAttachedToHierarchy();
super.onAttachedToHierarchy(preferenceManager);
}
public void checkRestrictionAndSetDisabled(String userRestriction) {
mHelper.checkRestrictionAndSetDisabled(userRestriction, UserHandle.myUserId());
}
public void checkRestrictionAndSetDisabled(String userRestriction, int userId) {
mHelper.checkRestrictionAndSetDisabled(userRestriction, userId);
}
/**
* Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this
* package. Marks the preference as disabled if so.
* @param settingIdentifier The key identifying the setting
* @param packageName the package to check the settingIdentifier for
*/
public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
@NonNull String packageName) {
mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName);
}
@Override
public void setEnabled(boolean enabled) {
if (enabled && isDisabledByAdmin()) {
mHelper.setDisabledByAdmin(null);
return;
}
if (enabled && isDisabledByEcm()) {
mHelper.setDisabledByEcm(null);
return;
}
super.setEnabled(enabled);
}
public void setDisabledByAdmin(EnforcedAdmin admin) {
if (mHelper.setDisabledByAdmin(admin)) {
notifyChanged();
}
}
public boolean isDisabledByAdmin() {
return mHelper.isDisabledByAdmin();
}
public boolean isDisabledByEcm() {
return mHelper.isDisabledByEcm();
}
public int getUid() {
return mHelper != null ? mHelper.uid : Process.INVALID_UID;
}
public String getPackageName() {
return mHelper != null ? mHelper.packageName : null;
}
/**
* @deprecated TODO(b/308921175): This will be deleted with the
* {@link android.security.Flags#extendEcmToAllSettings} feature flag. Do not use for any new
* code.
*/
@Deprecated
public void setDisabledByAppOps(boolean disabled) {
if (mHelper.setDisabledByAppOps(disabled)) {
notifyChanged();
}
}
}

View File

@@ -0,0 +1,312 @@
/*
* Copyright (C) 2016 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.settingslib;
import static android.app.admin.DevicePolicyResources.Strings.Settings.CONTROLLED_BY_ADMIN_SUMMARY;
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settingslib.utils.BuildCompatUtils;
/**
* Helper class for managing settings preferences that can be disabled
* by device admins via user restrictions.
*/
public class RestrictedPreferenceHelper {
private static final String TAG = "RestrictedPreferenceHelper";
private final Context mContext;
private final Preference mPreference;
String packageName;
/**
* @deprecated TODO(b/308921175): This will be deleted with the
* {@link android.security.Flags#extendEcmToAllSettings} feature flag. Do not use for any new
* code.
*/
int uid;
private boolean mDisabledByAdmin;
@VisibleForTesting
EnforcedAdmin mEnforcedAdmin;
private String mAttrUserRestriction = null;
private boolean mDisabledSummary = false;
private boolean mDisabledByEcm;
private Intent mDisabledByEcmIntent = null;
public RestrictedPreferenceHelper(Context context, Preference preference,
AttributeSet attrs, String packageName, int uid) {
mContext = context;
mPreference = preference;
this.packageName = packageName;
this.uid = uid;
if (attrs != null) {
final TypedArray attributes = context.obtainStyledAttributes(attrs,
R.styleable.RestrictedPreference);
final TypedValue userRestriction =
attributes.peekValue(R.styleable.RestrictedPreference_userRestriction);
CharSequence data = null;
if (userRestriction != null && userRestriction.type == TypedValue.TYPE_STRING) {
if (userRestriction.resourceId != 0) {
data = context.getText(userRestriction.resourceId);
} else {
data = userRestriction.string;
}
}
mAttrUserRestriction = data == null ? null : data.toString();
// If the system has set the user restriction, then we shouldn't add the padlock.
if (RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, mAttrUserRestriction,
UserHandle.myUserId())) {
mAttrUserRestriction = null;
return;
}
final TypedValue useAdminDisabledSummary =
attributes.peekValue(R.styleable.RestrictedPreference_useAdminDisabledSummary);
if (useAdminDisabledSummary != null) {
mDisabledSummary =
(useAdminDisabledSummary.type == TypedValue.TYPE_INT_BOOLEAN
&& useAdminDisabledSummary.data != 0);
}
}
}
public RestrictedPreferenceHelper(Context context, Preference preference,
AttributeSet attrs) {
this(context, preference, attrs, null, android.os.Process.INVALID_UID);
}
/**
* Modify PreferenceViewHolder to add padlock if restriction is disabled.
*/
public void onBindViewHolder(PreferenceViewHolder holder) {
if (mDisabledByAdmin || mDisabledByEcm) {
holder.itemView.setEnabled(true);
}
if (mDisabledSummary) {
final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary);
if (summaryView != null) {
final CharSequence disabledText = BuildCompatUtils.isAtLeastT()
? getDisabledByAdminUpdatableString()
: mContext.getString(R.string.disabled_by_admin_summary_text);
if (mDisabledByAdmin) {
summaryView.setText(disabledText);
} else if (mDisabledByEcm) {
summaryView.setText(R.string.disabled_by_app_ops_text);
} else if (TextUtils.equals(disabledText, summaryView.getText())) {
// It's previously set to disabled text, clear it.
summaryView.setText(null);
}
}
}
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private String getDisabledByAdminUpdatableString() {
return mContext.getSystemService(DevicePolicyManager.class).getResources().getString(
CONTROLLED_BY_ADMIN_SUMMARY,
() -> mContext.getString(R.string.disabled_by_admin_summary_text));
}
public void useAdminDisabledSummary(boolean useSummary) {
mDisabledSummary = useSummary;
}
/**
* Check if the preference is disabled if so handle the click by informing the user.
*
* @return true if the method handled the click.
*/
@SuppressWarnings("NewApi")
public boolean performClick() {
if (mDisabledByAdmin) {
RestrictedLockUtils.sendShowAdminSupportDetailsIntent(mContext, mEnforcedAdmin);
return true;
}
if (mDisabledByEcm) {
if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()
&& android.security.Flags.extendEcmToAllSettings()) {
mContext.startActivity(mDisabledByEcmIntent);
return true;
} else {
RestrictedLockUtilsInternal.sendShowRestrictedSettingDialogIntent(mContext,
packageName, uid);
return true;
}
}
return false;
}
/**
* Disable / enable if we have been passed the restriction in the xml.
*/
public void onAttachedToHierarchy() {
if (mAttrUserRestriction != null) {
checkRestrictionAndSetDisabled(mAttrUserRestriction, UserHandle.myUserId());
}
}
/**
* Set the user restriction that is used to disable this preference.
*
* @param userRestriction constant from {@link android.os.UserManager}
* @param userId user to check the restriction for.
*/
public void checkRestrictionAndSetDisabled(String userRestriction, int userId) {
EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
userRestriction, userId);
setDisabledByAdmin(admin);
}
/**
* Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this
* package. Marks the preference as disabled if so.
* @param settingIdentifier The key identifying the setting
* @param packageName the package to check the settingIdentifier for
*/
public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
@NonNull String packageName) {
updatePackageDetails(packageName, android.os.Process.INVALID_UID);
Intent intent = RestrictedLockUtilsInternal.checkIfRequiresEnhancedConfirmation(
mContext, settingIdentifier, packageName);
setDisabledByEcm(intent);
}
/**
* @return EnforcedAdmin if we have been passed the restriction in the xml.
*/
public EnforcedAdmin checkRestrictionEnforced() {
if (mAttrUserRestriction == null) {
return null;
}
return RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
mAttrUserRestriction, UserHandle.myUserId());
}
/**
* Disable this preference based on the enforce admin.
*
* @param admin details of the admin who enforced the restriction. If it is
* {@code null}, then this preference will be enabled. Otherwise, it will be disabled.
* Only gray out the preference which is not {@link RestrictedTopLevelPreference}.
* @return true if the disabled state was changed.
*/
public boolean setDisabledByAdmin(EnforcedAdmin admin) {
boolean disabled = false;
mEnforcedAdmin = null;
if (admin != null) {
disabled = true;
// Copy the received instance to prevent pass be reference being overwritten.
mEnforcedAdmin = new EnforcedAdmin(admin);
}
boolean changed = false;
if (mDisabledByAdmin != disabled) {
mDisabledByAdmin = disabled;
changed = true;
updateDisabledState();
}
return changed;
}
/**
* Disable the preference based on the passed in Intent
* @param disabledIntent The intent which is started when the user clicks the disabled
* preference. If it is {@code null}, then this preference will be enabled. Otherwise, it will
* be disabled.
* @return true if the disabled state was changed.
*/
public boolean setDisabledByEcm(@Nullable Intent disabledIntent) {
boolean disabled = disabledIntent != null;
boolean changed = false;
if (mDisabledByEcm != disabled) {
mDisabledByEcmIntent = disabledIntent;
mDisabledByEcm = disabled;
changed = true;
updateDisabledState();
}
return changed;
}
public boolean isDisabledByAdmin() {
return mDisabledByAdmin;
}
public boolean isDisabledByEcm() {
return mDisabledByEcm;
}
public void updatePackageDetails(String packageName, int uid) {
this.packageName = packageName;
this.uid = uid;
}
private void updateDisabledState() {
boolean isEnabled = !(mDisabledByAdmin || mDisabledByEcm);
if (!(mPreference instanceof RestrictedTopLevelPreference)) {
mPreference.setEnabled(isEnabled);
}
if (mPreference instanceof PrimarySwitchPreference) {
((PrimarySwitchPreference) mPreference).setSwitchEnabled(isEnabled);
}
if (!isEnabled && mDisabledByEcm) {
mPreference.setSummary(R.string.disabled_by_app_ops_text);
}
}
/**
* @deprecated TODO(b/308921175): This will be deleted with the
* {@link android.security.Flags#extendEcmToAllSettings} feature flag. Do not use for any new
* code.
*/
@Deprecated
public boolean setDisabledByAppOps(boolean disabled) {
boolean changed = false;
if (mDisabledByEcm != disabled) {
mDisabledByEcm = disabled;
changed = true;
updateDisabledState();
}
return changed;
}
}

View File

@@ -0,0 +1,303 @@
/*
* Copyright (C) 2016 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.settingslib;
import static android.app.admin.DevicePolicyResources.Strings.Settings.DISABLED_BY_ADMIN_SWITCH_SUMMARY;
import static android.app.admin.DevicePolicyResources.Strings.Settings.ENABLED_BY_ADMIN_SWITCH_SUMMARY;
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import android.app.AppOpsManager;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Process;
import android.os.UserHandle;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceViewHolder;
import androidx.preference.SwitchPreferenceCompat;
import com.android.settingslib.utils.BuildCompatUtils;
/**
* Version of SwitchPreferenceCompat that can be disabled by a device admin
* using a user restriction.
*/
public class RestrictedSwitchPreference extends SwitchPreferenceCompat {
RestrictedPreferenceHelper mHelper;
AppOpsManager mAppOpsManager;
boolean mUseAdditionalSummary = false;
CharSequence mRestrictedSwitchSummary;
private int mIconSize;
public RestrictedSwitchPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mHelper = new RestrictedPreferenceHelper(context, this, attrs);
if (attrs != null) {
final TypedArray attributes = context.obtainStyledAttributes(attrs,
R.styleable.RestrictedSwitchPreference);
final TypedValue useAdditionalSummary = attributes.peekValue(
R.styleable.RestrictedSwitchPreference_useAdditionalSummary);
if (useAdditionalSummary != null) {
mUseAdditionalSummary =
(useAdditionalSummary.type == TypedValue.TYPE_INT_BOOLEAN
&& useAdditionalSummary.data != 0);
}
final TypedValue restrictedSwitchSummary = attributes.peekValue(
R.styleable.RestrictedSwitchPreference_restrictedSwitchSummary);
attributes.recycle();
if (restrictedSwitchSummary != null
&& restrictedSwitchSummary.type == TypedValue.TYPE_STRING) {
if (restrictedSwitchSummary.resourceId != 0) {
mRestrictedSwitchSummary =
context.getText(restrictedSwitchSummary.resourceId);
} else {
mRestrictedSwitchSummary = restrictedSwitchSummary.string;
}
}
}
if (mUseAdditionalSummary) {
setLayoutResource(R.layout.restricted_switch_preference);
useAdminDisabledSummary(false);
}
}
public RestrictedSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public RestrictedSwitchPreference(Context context, AttributeSet attrs) {
this(context, attrs, androidx.preference.R.attr.switchPreferenceCompatStyle);
}
public RestrictedSwitchPreference(Context context) {
this(context, null);
}
@VisibleForTesting
public void setAppOps(AppOpsManager appOps) {
mAppOpsManager = appOps;
}
public void setIconSize(int iconSize) {
mIconSize = iconSize;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final View switchView = holder.findViewById(androidx.preference.R.id.switchWidget);
if (switchView != null) {
final View rootView = switchView.getRootView();
rootView.setFilterTouchesWhenObscured(true);
}
mHelper.onBindViewHolder(holder);
CharSequence switchSummary;
if (mRestrictedSwitchSummary == null) {
switchSummary = isChecked()
? getUpdatableEnterpriseString(
getContext(), ENABLED_BY_ADMIN_SWITCH_SUMMARY,
com.android.settingslib.widget.restricted.R.string.enabled_by_admin)
: getUpdatableEnterpriseString(
getContext(), DISABLED_BY_ADMIN_SWITCH_SUMMARY,
com.android.settingslib.widget.restricted.R.string.disabled_by_admin);
} else {
switchSummary = mRestrictedSwitchSummary;
}
final ImageView icon = holder.itemView.findViewById(android.R.id.icon);
if (mIconSize > 0) {
icon.setLayoutParams(new LinearLayout.LayoutParams(mIconSize, mIconSize));
}
if (mUseAdditionalSummary) {
final TextView additionalSummaryView = (TextView) holder.findViewById(
R.id.additional_summary);
if (additionalSummaryView != null) {
if (isDisabledByAdmin()) {
additionalSummaryView.setText(switchSummary);
additionalSummaryView.setVisibility(View.VISIBLE);
} else {
additionalSummaryView.setVisibility(View.GONE);
}
}
} else {
final TextView summaryView = (TextView) holder.findViewById(android.R.id.summary);
if (summaryView != null) {
if (isDisabledByAdmin()) {
summaryView.setText(switchSummary);
summaryView.setVisibility(View.VISIBLE);
}
// No need to change the visibility to GONE in the else case here since Preference
// class would have already changed it if there is no summary to display.
}
}
}
private static String getUpdatableEnterpriseString(
Context context, String updatableStringId, int resId) {
if (!BuildCompatUtils.isAtLeastT()) {
return context.getString(resId);
}
return context.getSystemService(DevicePolicyManager.class).getResources().getString(
updatableStringId,
() -> context.getString(resId));
}
@Override
public void performClick() {
if (!mHelper.performClick()) {
super.performClick();
}
}
public void useAdminDisabledSummary(boolean useSummary) {
mHelper.useAdminDisabledSummary(useSummary);
}
@Override
protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
mHelper.onAttachedToHierarchy();
super.onAttachedToHierarchy(preferenceManager);
}
public void checkRestrictionAndSetDisabled(String userRestriction) {
mHelper.checkRestrictionAndSetDisabled(userRestriction, UserHandle.myUserId());
}
public void checkRestrictionAndSetDisabled(String userRestriction, int userId) {
mHelper.checkRestrictionAndSetDisabled(userRestriction, userId);
}
/**
* Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this
* package. Marks the preference as disabled if so.
* @param settingIdentifier The key identifying the setting
* @param packageName the package to check the settingIdentifier for
*/
public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier,
@NonNull String packageName) {
mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName);
}
@Override
public void setEnabled(boolean enabled) {
boolean changed = false;
if (enabled && isDisabledByAdmin()) {
mHelper.setDisabledByAdmin(null);
changed = true;
}
if (enabled && isDisabledByEcm()) {
mHelper.setDisabledByEcm(null);
changed = true;
}
if (!changed) {
super.setEnabled(enabled);
}
}
public void setDisabledByAdmin(EnforcedAdmin admin) {
if (mHelper.setDisabledByAdmin(admin)) {
notifyChanged();
}
}
public boolean isDisabledByAdmin() {
return mHelper.isDisabledByAdmin();
}
public boolean isDisabledByEcm() {
return mHelper.isDisabledByEcm();
}
/**
* @deprecated TODO(b/308921175): This will be deleted with the
* {@link android.security.Flags#extendEcmToAllSettings} feature flag. Do not use for any new
* code.
*/
@Deprecated
private void setDisabledByAppOps(boolean disabled) {
if (mHelper.setDisabledByAppOps(disabled)) {
notifyChanged();
}
}
/**
* @deprecated TODO(b/308921175): This will be deleted with the
* {@link android.security.Flags#extendEcmToAllSettings} feature flag. Do not use for any new
* code.
*/
@Deprecated
public int getUid() {
return mHelper != null ? mHelper.uid : Process.INVALID_UID;
}
/**
* @deprecated TODO(b/308921175): This will be deleted with the
* {@link android.security.Flags#extendEcmToAllSettings} feature flag. Do not use for any new
* code.
*/
@Deprecated
public String getPackageName() {
return mHelper != null ? mHelper.packageName : null;
}
/**
* Updates enabled state based on associated package
*
* @deprecated TODO(b/308921175): This will be deleted with the
* {@link android.security.Flags#extendEcmToAllSettings} feature flag. Do not use for any new
* code.
*/
@Deprecated
public void updateState(
@NonNull String packageName, int uid, boolean isEnableAllowed, boolean isEnabled) {
mHelper.updatePackageDetails(packageName, uid);
if (mAppOpsManager == null) {
mAppOpsManager = getContext().getSystemService(AppOpsManager.class);
}
final int mode = mAppOpsManager.noteOpNoThrow(
AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS,
uid, packageName);
final boolean ecmEnabled = getContext().getResources().getBoolean(
com.android.internal.R.bool.config_enhancedConfirmationModeEnabled);
final boolean appOpsAllowed = !ecmEnabled || mode == AppOpsManager.MODE_ALLOWED;
if (!isEnableAllowed && !isEnabled) {
setEnabled(false);
} else if (isEnabled) {
setEnabled(true);
} else if (appOpsAllowed && isDisabledByEcm()) {
setEnabled(true);
} else if (!appOpsAllowed){
setDisabledByAppOps(true);
}
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright (C) 2020 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.settingslib;
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import android.content.Context;
import android.os.UserHandle;
import android.util.AttributeSet;
import androidx.core.content.res.TypedArrayUtils;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceViewHolder;
/** Top level preference that can be disabled by a device admin using a user restriction. */
public class RestrictedTopLevelPreference extends Preference {
private RestrictedPreferenceHelper mHelper;
public RestrictedTopLevelPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mHelper = new RestrictedPreferenceHelper(context, /* preference= */ this, attrs);
}
public RestrictedTopLevelPreference(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, /* defStyleRes= */ 0);
}
public RestrictedTopLevelPreference(Context context, AttributeSet attrs) {
this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.preferenceStyle,
android.R.attr.preferenceStyle));
}
public RestrictedTopLevelPreference(Context context) {
this(context, /* attrs= */ null);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
mHelper.onBindViewHolder(holder);
}
@Override
public void performClick() {
if (!mHelper.performClick()) {
super.performClick();
}
}
@Override
protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
mHelper.onAttachedToHierarchy();
super.onAttachedToHierarchy(preferenceManager);
}
/**
* Set the user restriction and disable this preference.
*
* @param userRestriction constant from {@link android.os.UserManager}
*/
public void checkRestrictionAndSetDisabled(String userRestriction) {
mHelper.checkRestrictionAndSetDisabled(userRestriction, UserHandle.myUserId());
}
/**
* Set the user restriction and disable this preference for the given user.
*
* @param userRestriction constant from {@link android.os.UserManager}
* @param userId user to check the restriction for.
*/
public void checkRestrictionAndSetDisabled(String userRestriction, int userId) {
mHelper.checkRestrictionAndSetDisabled(userRestriction, userId);
}
@Override
public void setEnabled(boolean enabled) {
if (enabled && isDisabledByAdmin()) {
mHelper.setDisabledByAdmin(/* admin= */ null);
return;
}
super.setEnabled(enabled);
}
/**
* Check whether this preference is disabled by admin.
*
* @return true if this preference is disabled by admin.
*/
public boolean isDisabledByAdmin() {
return mHelper.isDisabledByAdmin();
}
/**
* Disable preference based on the enforce admin.
*
* @param admin details of the admin who enforced the restriction. If it is {@code null}, then
* this preference will be enabled. Otherwise, it will be disabled.
*/
public void setDisabledByAdmin(EnforcedAdmin admin) {
if (mHelper.setDisabledByAdmin(admin)) {
notifyChanged();
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2020 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.settingslib;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
/**
* Icons for SysUI and Settings.
*/
public class SignalIcon {
/**
* Holds icons for a given state. Arrays are generally indexed as inet
* state (full connectivity or not) first, and second dimension as
* signal strength.
*/
public static class IconGroup {
public final int[][] sbIcons;
public final int[][] qsIcons;
public final int[] contentDesc;
public final int sbNullState;
public final int qsNullState;
public final int sbDiscState;
public final int qsDiscState;
public final int discContentDesc;
// For logging.
public final String name;
public IconGroup(
String name,
int[][] sbIcons,
int[][] qsIcons,
int[] contentDesc,
int sbNullState,
int qsNullState,
int sbDiscState,
int qsDiscState,
int discContentDesc
) {
this.name = name;
this.sbIcons = sbIcons;
this.qsIcons = qsIcons;
this.contentDesc = contentDesc;
this.sbNullState = sbNullState;
this.qsNullState = qsNullState;
this.sbDiscState = sbDiscState;
this.qsDiscState = qsDiscState;
this.discContentDesc = discContentDesc;
}
@Override
public String toString() {
return "IconGroup(" + name + ")";
}
}
/**
* Holds RAT icons for a given MobileState.
*/
public static class MobileIconGroup extends IconGroup {
@StringRes public final int dataContentDescription;
@DrawableRes public final int dataType;
public MobileIconGroup(
String name,
int dataContentDesc,
int dataType
) {
super(name,
// The rest of the values are the same for every type of MobileIconGroup, so
// just provide them here.
// TODO(b/238425913): Eventually replace with {@link MobileNetworkTypeIcon} so
// that we don't have to fill in these superfluous fields.
/* sbIcons= */ null,
/* qsIcons= */ null,
/* contentDesc= */ AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH,
/* sbNullState= */ 0,
/* qsNullState= */ 0,
/* sbDiscState= */ 0,
/* qsDiscState= */ 0,
/* discContentDesc= */
AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH_NONE);
this.dataContentDescription = dataContentDesc;
this.dataType = dataType;
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2018 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.settingslib;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Process;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Log;
import java.util.Set;
/**
* Utility class that allows Settings to use SystemUI to relay broadcasts related to pinned slices.
*/
public class SliceBroadcastRelay {
public static final String ACTION_REGISTER
= "com.android.settingslib.action.REGISTER_SLICE_RECEIVER";
public static final String ACTION_UNREGISTER
= "com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER";
public static final String SYSTEMUI_PACKAGE = "com.android.systemui";
public static final String EXTRA_URI = "uri";
public static final String EXTRA_RECEIVER = "receiver";
public static final String EXTRA_FILTER = "filter";
private static final String TAG = "SliceBroadcastRelay";
private static final Set<Uri> sRegisteredUris = new ArraySet<>();
/**
* Associate intent filter/sliceUri with corresponding receiver.
*/
public static void registerReceiver(Context context, Uri sliceUri,
Class<? extends BroadcastReceiver> receiver, IntentFilter filter) {
Log.d(TAG, "Registering Uri for broadcast relay: " + sliceUri);
sRegisteredUris.add(sliceUri);
Intent registerBroadcast = new Intent(ACTION_REGISTER);
registerBroadcast.setPackage(SYSTEMUI_PACKAGE);
registerBroadcast.putExtra(EXTRA_URI, ContentProvider.maybeAddUserId(sliceUri,
Process.myUserHandle().getIdentifier()));
registerBroadcast.putExtra(EXTRA_RECEIVER,
new ComponentName(context.getPackageName(), receiver.getName()));
registerBroadcast.putExtra(EXTRA_FILTER, filter);
context.sendBroadcastAsUser(registerBroadcast, UserHandle.SYSTEM);
}
/**
* Unregisters all receivers for a given slice uri.
*/
public static void unregisterReceivers(Context context, Uri sliceUri) {
if (!sRegisteredUris.contains(sliceUri)) {
return;
}
Log.d(TAG, "Unregistering uri broadcast relay: " + sliceUri);
final Intent registerBroadcast = new Intent(ACTION_UNREGISTER);
registerBroadcast.setPackage(SYSTEMUI_PACKAGE);
registerBroadcast.putExtra(EXTRA_URI, ContentProvider.maybeAddUserId(sliceUri,
Process.myUserHandle().getIdentifier()));
context.sendBroadcastAsUser(registerBroadcast, UserHandle.SYSTEM);
sRegisteredUris.remove(sliceUri);
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib;
import static android.os.UserManager.DISALLOW_CONFIG_TETHERING;
import android.content.Context;
import android.net.ConnectivityManager;
import android.os.UserHandle;
public class TetherUtil {
public static boolean isTetherAvailable(Context context) {
final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
final boolean tetherConfigDisallowed = RestrictedLockUtilsInternal
.checkIfRestrictionEnforced(context, DISALLOW_CONFIG_TETHERING,
UserHandle.myUserId()) != null;
final boolean hasBaseUserRestriction = RestrictedLockUtilsInternal.hasBaseUserRestriction(
context, DISALLOW_CONFIG_TETHERING, UserHandle.myUserId());
return (cm.isTetheringSupported() || tetherConfigDisallowed) && !hasBaseUserRestriction;
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib;
import android.content.Context;
import com.android.internal.logging.MetricsLogger;
import com.android.settingslib.wifi.AccessPoint.Speed;
/** Utilites for Tron Logging. */
public final class TronUtils {
private static final String TAG = "TronUtils";
private TronUtils() {};
public static void logWifiSettingsSpeed(Context context, @Speed int speedEnum) {
MetricsLogger.histogram(context, "settings_wifi_speed_labels", speedEnum);
}
}

View File

@@ -0,0 +1,869 @@
package com.android.settingslib;
import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_USER_LABEL;
import static android.webkit.Flags.updateServiceV2;
import android.annotation.ColorInt;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.content.pm.UserInfo;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.drawable.Drawable;
import android.hardware.usb.UsbManager;
import android.hardware.usb.UsbPort;
import android.hardware.usb.UsbPortStatus;
import android.hardware.usb.flags.Flags;
import android.location.LocationManager;
import android.media.AudioManager;
import android.net.NetworkCapabilities;
import android.net.TetheringManager;
import android.net.vcn.VcnTransportInfo;
import android.net.wifi.WifiInfo;
import android.os.BatteryManager;
import android.os.Build;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.print.PrintManager;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.telephony.AccessNetworkConstants;
import android.telephony.NetworkRegistrationInfo;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.webkit.IWebViewUpdateService;
import android.webkit.WebViewFactory;
import android.webkit.WebViewProviderInfo;
import android.webkit.WebViewUpdateManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.UserIcons;
import com.android.launcher3.icons.BaseIconFactory.IconOptions;
import com.android.launcher3.icons.IconFactory;
import com.android.launcher3.util.UserIconInfo;
import com.android.settingslib.drawable.UserIconDrawable;
import com.android.settingslib.fuelgauge.BatteryStatus;
import com.android.settingslib.utils.BuildCompatUtils;
import java.text.NumberFormat;
import java.time.Duration;
import java.util.List;
public class Utils {
private static final String TAG = "Utils";
public static final String INCOMPATIBLE_CHARGER_WARNING_DISABLED =
"incompatible_charger_warning_disabled";
public static final String WIRELESS_CHARGING_NOTIFICATION_TIMESTAMP =
"wireless_charging_notification_timestamp";
@VisibleForTesting
static final String STORAGE_MANAGER_ENABLED_PROPERTY = "ro.storage_manager.enabled";
@VisibleForTesting static final long WIRELESS_CHARGING_DEFAULT_TIMESTAMP = -1L;
@VisibleForTesting
static final long WIRELESS_CHARGING_NOTIFICATION_THRESHOLD_MILLIS =
Duration.ofDays(30).toMillis();
@VisibleForTesting
static final String WIRELESS_CHARGING_WARNING_ENABLED = "wireless_charging_warning_enabled";
private static Signature[] sSystemSignature;
private static String sPermissionControllerPackageName;
private static String sServicesSystemSharedLibPackageName;
private static String sSharedSystemSharedLibPackageName;
private static String sDefaultWebViewPackageName;
static final int[] WIFI_PIE = {
com.android.internal.R.drawable.ic_wifi_signal_0,
com.android.internal.R.drawable.ic_wifi_signal_1,
com.android.internal.R.drawable.ic_wifi_signal_2,
com.android.internal.R.drawable.ic_wifi_signal_3,
com.android.internal.R.drawable.ic_wifi_signal_4
};
static final int[] SHOW_X_WIFI_PIE = {
R.drawable.ic_show_x_wifi_signal_0,
R.drawable.ic_show_x_wifi_signal_1,
R.drawable.ic_show_x_wifi_signal_2,
R.drawable.ic_show_x_wifi_signal_3,
R.drawable.ic_show_x_wifi_signal_4
};
/** Update the location enable state. */
public static void updateLocationEnabled(
@NonNull Context context, boolean enabled, int userId, int source) {
Settings.Secure.putIntForUser(
context.getContentResolver(), Settings.Secure.LOCATION_CHANGER, source, userId);
LocationManager locationManager = context.getSystemService(LocationManager.class);
locationManager.setLocationEnabledForUser(enabled, UserHandle.of(userId));
}
/**
* Return string resource that best describes combination of tethering options available on this
* device.
*/
public static int getTetheringLabel(TetheringManager tm) {
String[] usbRegexs = tm.getTetherableUsbRegexs();
String[] wifiRegexs = tm.getTetherableWifiRegexs();
String[] bluetoothRegexs = tm.getTetherableBluetoothRegexs();
boolean usbAvailable = usbRegexs.length != 0;
boolean wifiAvailable = wifiRegexs.length != 0;
boolean bluetoothAvailable = bluetoothRegexs.length != 0;
if (wifiAvailable && usbAvailable && bluetoothAvailable) {
return R.string.tether_settings_title_all;
} else if (wifiAvailable && usbAvailable) {
return R.string.tether_settings_title_all;
} else if (wifiAvailable && bluetoothAvailable) {
return R.string.tether_settings_title_all;
} else if (wifiAvailable) {
return R.string.tether_settings_title_wifi;
} else if (usbAvailable && bluetoothAvailable) {
return R.string.tether_settings_title_usb_bluetooth;
} else if (usbAvailable) {
return R.string.tether_settings_title_usb;
} else {
return R.string.tether_settings_title_bluetooth;
}
}
/** Returns a label for the user, in the form of "User: user name" or "Work profile". */
public static String getUserLabel(Context context, UserInfo info) {
String name = info != null ? info.name : null;
if (info.isManagedProfile()) {
// We use predefined values for managed profiles
return BuildCompatUtils.isAtLeastT()
? getUpdatableManagedUserTitle(context)
: context.getString(R.string.managed_user_title);
} else if (info.isGuest()) {
name = context.getString(com.android.internal.R.string.guest_name);
}
if (name == null && info != null) {
name = Integer.toString(info.id);
} else if (info == null) {
name = context.getString(R.string.unknown);
}
return context.getResources().getString(R.string.running_process_item_user_label, name);
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private static String getUpdatableManagedUserTitle(Context context) {
return context.getSystemService(DevicePolicyManager.class)
.getResources()
.getString(
WORK_PROFILE_USER_LABEL,
() -> context.getString(R.string.managed_user_title));
}
/** Returns a circular icon for a user. */
public static Drawable getUserIcon(Context context, UserManager um, UserInfo user) {
final int iconSize = UserIconDrawable.getDefaultSize(context);
if (user.isManagedProfile()) {
Drawable drawable = UserIconDrawable.getManagedUserDrawable(context);
drawable.setBounds(0, 0, iconSize, iconSize);
return drawable;
}
if (user.iconPath != null) {
Bitmap icon = um.getUserIcon(user.id);
if (icon != null) {
return new UserIconDrawable(iconSize).setIcon(icon).bake();
}
}
return new UserIconDrawable(iconSize)
.setIconDrawable(
UserIcons.getDefaultUserIcon(
context.getResources(), user.id, /* light= */ false))
.bake();
}
/** Formats a double from 0.0..100.0 with an option to round */
public static String formatPercentage(double percentage, boolean round) {
final int localPercentage = round ? Math.round((float) percentage) : (int) percentage;
return formatPercentage(localPercentage);
}
/** Formats the ratio of amount/total as a percentage. */
public static String formatPercentage(long amount, long total) {
return formatPercentage(((double) amount) / total);
}
/** Formats an integer from 0..100 as a percentage. */
public static String formatPercentage(int percentage) {
return formatPercentage(((double) percentage) / 100.0);
}
/** Formats a double from 0.0..1.0 as a percentage. */
public static String formatPercentage(double percentage) {
return NumberFormat.getPercentInstance().format(percentage);
}
public static int getBatteryLevel(Intent batteryChangedIntent) {
int level = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
int scale = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
return (level * 100) / scale;
}
/**
* Get battery status string
*
* @param context the context
* @param batteryChangedIntent battery broadcast intent received from {@link
* Intent.ACTION_BATTERY_CHANGED}.
* @param compactStatus to present compact battery charging string if {@code true}
* @return battery status string
*/
@NonNull
public static String getBatteryStatus(
@NonNull Context context, @NonNull Intent batteryChangedIntent, boolean compactStatus) {
final int status =
batteryChangedIntent.getIntExtra(
BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN);
final Resources res = context.getResources();
String statusString = res.getString(R.string.battery_info_status_unknown);
final BatteryStatus batteryStatus = new BatteryStatus(batteryChangedIntent);
if (batteryStatus.isCharged()) {
statusString =
res.getString(
compactStatus
? R.string.battery_info_status_full_charged
: R.string.battery_info_status_full);
} else {
if (status == BatteryManager.BATTERY_STATUS_CHARGING) {
if (compactStatus) {
statusString = res.getString(R.string.battery_info_status_charging);
} else if (batteryStatus.isPluggedInWired()) {
switch (batteryStatus.getChargingSpeed(context)) {
case BatteryStatus.CHARGING_FAST:
statusString =
res.getString(R.string.battery_info_status_charging_fast);
break;
case BatteryStatus.CHARGING_SLOWLY:
statusString =
res.getString(R.string.battery_info_status_charging_slow);
break;
default:
statusString = res.getString(R.string.battery_info_status_charging);
break;
}
} else if (batteryStatus.isPluggedInDock()) {
statusString = res.getString(R.string.battery_info_status_charging_dock);
} else {
statusString = res.getString(R.string.battery_info_status_charging_wireless);
}
} else if (status == BatteryManager.BATTERY_STATUS_DISCHARGING) {
statusString = res.getString(R.string.battery_info_status_discharging);
} else if (status == BatteryManager.BATTERY_STATUS_NOT_CHARGING) {
statusString = res.getString(R.string.battery_info_status_not_charging);
}
}
return statusString;
}
public static ColorStateList getColorAccent(Context context) {
return getColorAttr(context, android.R.attr.colorAccent);
}
public static ColorStateList getColorError(Context context) {
return getColorAttr(context, android.R.attr.colorError);
}
@ColorInt
public static int getColorAccentDefaultColor(Context context) {
return getColorAttrDefaultColor(context, android.R.attr.colorAccent);
}
@ColorInt
public static int getColorErrorDefaultColor(Context context) {
return getColorAttrDefaultColor(context, android.R.attr.colorError);
}
@ColorInt
public static int getColorStateListDefaultColor(Context context, int resId) {
final ColorStateList list =
context.getResources().getColorStateList(resId, context.getTheme());
return list.getDefaultColor();
}
/**
* This method computes disabled color from normal color
*
* @param context the context
* @param inputColor normal color.
* @return disabled color.
*/
@ColorInt
public static int getDisabled(Context context, int inputColor) {
return applyAlphaAttr(context, android.R.attr.disabledAlpha, inputColor);
}
@ColorInt
public static int applyAlphaAttr(Context context, int attr, int inputColor) {
TypedArray ta = context.obtainStyledAttributes(new int[] {attr});
float alpha = ta.getFloat(0, 0);
ta.recycle();
return applyAlpha(alpha, inputColor);
}
@ColorInt
public static int applyAlpha(float alpha, int inputColor) {
alpha *= Color.alpha(inputColor);
return Color.argb(
(int) (alpha),
Color.red(inputColor),
Color.green(inputColor),
Color.blue(inputColor));
}
@ColorInt
public static int getColorAttrDefaultColor(Context context, int attr) {
return getColorAttrDefaultColor(context, attr, 0);
}
/** Get color styled attribute {@code attr}, default to {@code defValue} if not found. */
@ColorInt
public static int getColorAttrDefaultColor(Context context, int attr, @ColorInt int defValue) {
TypedArray ta = context.obtainStyledAttributes(new int[] {attr});
@ColorInt int colorAccent = ta.getColor(0, defValue);
ta.recycle();
return colorAccent;
}
public static ColorStateList getColorAttr(Context context, int attr) {
TypedArray ta = context.obtainStyledAttributes(new int[] {attr});
ColorStateList stateList = null;
try {
stateList = ta.getColorStateList(0);
} finally {
ta.recycle();
}
return stateList;
}
public static int getThemeAttr(Context context, int attr) {
return getThemeAttr(context, attr, 0);
}
public static int getThemeAttr(Context context, int attr, int defaultValue) {
TypedArray ta = context.obtainStyledAttributes(new int[] {attr});
int theme = ta.getResourceId(0, defaultValue);
ta.recycle();
return theme;
}
public static Drawable getDrawable(Context context, int attr) {
TypedArray ta = context.obtainStyledAttributes(new int[] {attr});
Drawable drawable = ta.getDrawable(0);
ta.recycle();
return drawable;
}
/**
* Create a color matrix suitable for a ColorMatrixColorFilter that modifies only the color but
* preserves the alpha for a given drawable
*
* @return a color matrix that uses the source alpha and given color
*/
public static ColorMatrix getAlphaInvariantColorMatrixForColor(@ColorInt int color) {
int r = Color.red(color);
int g = Color.green(color);
int b = Color.blue(color);
ColorMatrix cm =
new ColorMatrix(
new float[] {
0, 0, 0, 0, r,
0, 0, 0, 0, g,
0, 0, 0, 0, b,
0, 0, 0, 1, 0
});
return cm;
}
/**
* Create a ColorMatrixColorFilter to tint a drawable but retain its alpha characteristics
*
* @return a ColorMatrixColorFilter which changes the color of the output but is invariant on
* the source alpha
*/
public static ColorFilter getAlphaInvariantColorFilterForColor(@ColorInt int color) {
return new ColorMatrixColorFilter(getAlphaInvariantColorMatrixForColor(color));
}
/**
* Determine whether a package is a "system package", in which case certain things (like
* disabling notifications or disabling the package altogether) should be disallowed.
*
* <p>Note: This function is just for UI treatment, and should not be used for security
* purposes.
*
* @deprecated Use {@link ApplicationInfo#isSignedWithPlatformKey()} and {@link
* #isEssentialPackage} instead.
*/
@Deprecated
public static boolean isSystemPackage(Resources resources, PackageManager pm, PackageInfo pkg) {
if (sSystemSignature == null) {
sSystemSignature = new Signature[] {getSystemSignature(pm)};
}
return (sSystemSignature[0] != null && sSystemSignature[0].equals(getFirstSignature(pkg)))
|| isEssentialPackage(resources, pm, pkg.packageName);
}
private static Signature getFirstSignature(PackageInfo pkg) {
if (pkg != null && pkg.signatures != null && pkg.signatures.length > 0) {
return pkg.signatures[0];
}
return null;
}
private static Signature getSystemSignature(PackageManager pm) {
try {
final PackageInfo sys = pm.getPackageInfo("android", PackageManager.GET_SIGNATURES);
return getFirstSignature(sys);
} catch (NameNotFoundException e) {
}
return null;
}
/**
* Determine whether a package is a "essential package".
*
* <p>In which case certain things (like disabling the package) should be disallowed.
*/
public static boolean isEssentialPackage(
Resources resources, PackageManager pm, String packageName) {
if (sPermissionControllerPackageName == null) {
sPermissionControllerPackageName = pm.getPermissionControllerPackageName();
}
if (sServicesSystemSharedLibPackageName == null) {
sServicesSystemSharedLibPackageName = pm.getServicesSystemSharedLibraryPackageName();
}
if (sSharedSystemSharedLibPackageName == null) {
sSharedSystemSharedLibPackageName = pm.getSharedSystemSharedLibraryPackageName();
}
return packageName.equals(sPermissionControllerPackageName)
|| packageName.equals(sServicesSystemSharedLibPackageName)
|| packageName.equals(sSharedSystemSharedLibPackageName)
|| packageName.equals(PrintManager.PRINT_SPOOLER_PACKAGE_NAME)
|| (updateServiceV2() && packageName.equals(getDefaultWebViewPackageName()))
|| isDeviceProvisioningPackage(resources, packageName);
}
/**
* Returns {@code true} if the supplied package is the device provisioning app. Otherwise,
* returns {@code false}.
*/
public static boolean isDeviceProvisioningPackage(Resources resources, String packageName) {
String deviceProvisioningPackage =
resources.getString(com.android.internal.R.string.config_deviceProvisioningPackage);
return deviceProvisioningPackage != null && deviceProvisioningPackage.equals(packageName);
}
/** Fetch the package name of the default WebView provider. */
@Nullable
private static String getDefaultWebViewPackageName() {
if (sDefaultWebViewPackageName != null) {
return sDefaultWebViewPackageName;
}
WebViewProviderInfo provider = null;
if (android.webkit.Flags.updateServiceIpcWrapper()) {
WebViewUpdateManager manager = WebViewUpdateManager.getInstance();
if (manager != null) {
provider = manager.getDefaultWebViewPackage();
}
} else {
try {
IWebViewUpdateService service = WebViewFactory.getUpdateService();
if (service != null) {
provider = service.getDefaultWebViewPackage();
}
} catch (RemoteException e) {
Log.e(TAG, "RemoteException when trying to fetch default WebView package Name", e);
}
}
if (provider != null) {
sDefaultWebViewPackageName = provider.packageName;
}
return sDefaultWebViewPackageName;
}
/**
* Returns the Wifi icon resource for a given RSSI level.
*
* @param level The number of bars to show (0-4)
* @throws IllegalArgumentException if an invalid RSSI level is given.
*/
public static int getWifiIconResource(int level) {
return getWifiIconResource(false /* showX */, level);
}
/**
* Returns the Wifi icon resource for a given RSSI level.
*
* @param showX True if a connected Wi-Fi network has the problem which should show Pie+x signal
* icon to users.
* @param level The number of bars to show (0-4)
* @throws IllegalArgumentException if an invalid RSSI level is given.
*/
public static int getWifiIconResource(boolean showX, int level) {
if (level < 0 || level >= WIFI_PIE.length) {
throw new IllegalArgumentException("No Wifi icon found for level: " + level);
}
return showX ? SHOW_X_WIFI_PIE[level] : WIFI_PIE[level];
}
public static int getDefaultStorageManagerDaysToRetain(Resources resources) {
int defaultDays = Settings.Secure.AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN_DEFAULT;
try {
defaultDays =
resources.getInteger(
com.android.internal.R.integer
.config_storageManagerDaystoRetainDefault);
} catch (Resources.NotFoundException e) {
// We are likely in a test environment.
}
return defaultDays;
}
public static boolean isWifiOnly(Context context) {
return !context.getSystemService(TelephonyManager.class).isDataCapable();
}
/** Returns if the automatic storage management feature is turned on or not. */
public static boolean isStorageManagerEnabled(Context context) {
boolean isDefaultOn;
try {
isDefaultOn = SystemProperties.getBoolean(STORAGE_MANAGER_ENABLED_PROPERTY, false);
} catch (Resources.NotFoundException e) {
isDefaultOn = false;
}
return Settings.Secure.getInt(
context.getContentResolver(),
Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED,
isDefaultOn ? 1 : 0)
!= 0;
}
/** get that {@link AudioManager#getMode()} is in ringing/call/communication(VoIP) status. */
public static boolean isAudioModeOngoingCall(Context context) {
final AudioManager audioManager = context.getSystemService(AudioManager.class);
final int audioMode = audioManager.getMode();
return audioMode == AudioManager.MODE_RINGTONE
|| audioMode == AudioManager.MODE_IN_CALL
|| audioMode == AudioManager.MODE_IN_COMMUNICATION;
}
/**
* Return the service state is in-service or not. To make behavior consistent with SystemUI and
* Settings/AboutPhone/SIM status UI
*
* @param serviceState Service state. {@link ServiceState}
*/
public static boolean isInService(ServiceState serviceState) {
if (serviceState == null) {
return false;
}
int state = getCombinedServiceState(serviceState);
if (state == ServiceState.STATE_POWER_OFF
|| state == ServiceState.STATE_OUT_OF_SERVICE
|| state == ServiceState.STATE_EMERGENCY_ONLY) {
return false;
} else {
return true;
}
}
/**
* Return the combined service state. To make behavior consistent with SystemUI and
* Settings/AboutPhone/SIM status UI.
*
* <p>This method returns a single service state int if either the voice reg state is {@link
* ServiceState#STATE_IN_SERVICE} or if data network is registered via a WWAN transport type. We
* consider the combined service state of an IWLAN network to be OOS.
*
* @param serviceState Service state. {@link ServiceState}
*/
public static int getCombinedServiceState(ServiceState serviceState) {
if (serviceState == null) {
return ServiceState.STATE_OUT_OF_SERVICE;
}
final int voiceRegState = serviceState.getVoiceRegState();
// Consider a mobile connection to be "in service" if either voice is IN_SERVICE
// or the data registration reports IN_SERVICE on a transport type of WWAN. This
// effectively excludes the IWLAN condition. IWLAN connections imply service via
// Wi-Fi rather than cellular, and so we do not consider these transports when
// determining if cellular is "in service".
if (voiceRegState == ServiceState.STATE_OUT_OF_SERVICE
|| voiceRegState == ServiceState.STATE_EMERGENCY_ONLY) {
if (isDataRegInWwanAndInService(serviceState)) {
return ServiceState.STATE_IN_SERVICE;
}
}
return voiceRegState;
}
// ServiceState#mDataRegState can be set to IN_SERVICE if the network is registered
// on either a WLAN or WWAN network. Since we want to exclude the WLAN network, we can
// query the WWAN network directly and check for its registration state
private static boolean isDataRegInWwanAndInService(ServiceState serviceState) {
final NetworkRegistrationInfo networkRegWwan =
serviceState.getNetworkRegistrationInfo(
NetworkRegistrationInfo.DOMAIN_PS,
AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
if (networkRegWwan == null) {
return false;
}
return networkRegWwan.isInService();
}
/** Get the corresponding adaptive icon drawable. */
public static Drawable getBadgedIcon(Context context, Drawable icon, UserHandle user) {
int userType = UserIconInfo.TYPE_MAIN;
try {
UserInfo ui =
context.getSystemService(UserManager.class).getUserInfo(user.getIdentifier());
if (ui != null) {
if (ui.isCloneProfile()) {
userType = UserIconInfo.TYPE_CLONED;
} else if (ui.isManagedProfile()) {
userType = UserIconInfo.TYPE_WORK;
} else if (ui.isPrivateProfile()) {
userType = UserIconInfo.TYPE_PRIVATE;
}
}
} catch (Exception e) {
// Ignore
}
try (IconFactory iconFactory = IconFactory.obtain(context)) {
return iconFactory
.createBadgedIconBitmap(
icon, new IconOptions().setUser(new UserIconInfo(user, userType)))
.newIcon(context);
}
}
/** Get the {@link Drawable} that represents the app icon */
public static Drawable getBadgedIcon(Context context, ApplicationInfo appInfo) {
return getBadgedIcon(
context,
appInfo.loadUnbadgedIcon(context.getPackageManager()),
UserHandle.getUserHandleForUid(appInfo.uid));
}
/**
* Returns a bitmap with rounded corner.
*
* @param context application context.
* @param source bitmap to apply round corner.
* @param cornerRadius corner radius value.
*/
@NonNull
public static Bitmap convertCornerRadiusBitmap(
@NonNull Context context, @NonNull Bitmap source, @NonNull float cornerRadius) {
final Bitmap roundedBitmap =
Bitmap.createBitmap(source.getWidth(), source.getHeight(), Bitmap.Config.ARGB_8888);
final RoundedBitmapDrawable drawable =
RoundedBitmapDrawableFactory.create(context.getResources(), source);
drawable.setAntiAlias(true);
drawable.setCornerRadius(cornerRadius);
final Canvas canvas = new Canvas(roundedBitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return roundedBitmap;
}
/**
* Returns the WifiInfo for the underlying WiFi network of the VCN network, returns null if the
* input NetworkCapabilities is not for a VCN network with underlying WiFi network.
*
* @param networkCapabilities NetworkCapabilities of the network.
*/
@Nullable
public static WifiInfo tryGetWifiInfoForVcn(NetworkCapabilities networkCapabilities) {
if (networkCapabilities.getTransportInfo() == null
|| !(networkCapabilities.getTransportInfo() instanceof VcnTransportInfo)) {
return null;
}
VcnTransportInfo vcnTransportInfo =
(VcnTransportInfo) networkCapabilities.getTransportInfo();
return vcnTransportInfo.getWifiInfo();
}
/** Whether there is any incompatible chargers in the current UsbPort? */
public static boolean containsIncompatibleChargers(Context context, String tag) {
// Avoid the caller doesn't have permission to read the "Settings.Secure" data.
try {
// Whether the incompatible charger warning is disabled or not
if (Settings.Secure.getInt(
context.getContentResolver(), INCOMPATIBLE_CHARGER_WARNING_DISABLED, 0)
== 1) {
Log.d(tag, "containsIncompatibleChargers: disabled");
return false;
}
} catch (Exception e) {
Log.e(tag, "containsIncompatibleChargers()", e);
return false;
}
final List<UsbPort> usbPortList = context.getSystemService(UsbManager.class).getPorts();
if (usbPortList == null || usbPortList.isEmpty()) {
return false;
}
for (UsbPort usbPort : usbPortList) {
Log.d(tag, "usbPort: " + usbPort);
if (!usbPort.supportsComplianceWarnings()) {
continue;
}
final UsbPortStatus usbStatus = usbPort.getStatus();
if (usbStatus == null || !usbStatus.isConnected()) {
continue;
}
final int[] complianceWarnings = usbStatus.getComplianceWarnings();
if (complianceWarnings == null || complianceWarnings.length == 0) {
continue;
}
for (int complianceWarningType : complianceWarnings) {
if (Flags.enableUsbDataComplianceWarning()
&& Flags.enableInputPowerLimitedWarning()) {
switch (complianceWarningType) {
case UsbPortStatus.COMPLIANCE_WARNING_INPUT_POWER_LIMITED:
case UsbPortStatus.COMPLIANCE_WARNING_DEBUG_ACCESSORY:
return true;
default:
break;
}
} else {
switch (complianceWarningType) {
case UsbPortStatus.COMPLIANCE_WARNING_OTHER:
case UsbPortStatus.COMPLIANCE_WARNING_DEBUG_ACCESSORY:
return true;
default:
break;
}
}
}
}
return false;
}
/** Whether to show the wireless charging notification. */
public static boolean shouldShowWirelessChargingNotification(
@NonNull Context context, @NonNull String tag) {
try {
return shouldShowWirelessChargingNotificationInternal(context, tag);
} catch (Exception e) {
Log.e(tag, "shouldShowWirelessChargingNotification()", e);
return false;
}
}
/** Stores the timestamp of the wireless charging notification. */
public static void updateWirelessChargingNotificationTimestamp(
@NonNull Context context, long timestamp, @NonNull String tag) {
try {
Secure.putLong(
context.getContentResolver(),
WIRELESS_CHARGING_NOTIFICATION_TIMESTAMP,
timestamp);
} catch (Exception e) {
Log.e(tag, "setWirelessChargingNotificationTimestamp()", e);
}
}
/** Whether to show the wireless charging warning in Settings. */
public static boolean shouldShowWirelessChargingWarningTip(
@NonNull Context context, @NonNull String tag) {
try {
return Secure.getInt(context.getContentResolver(), WIRELESS_CHARGING_WARNING_ENABLED, 0)
== 1;
} catch (Exception e) {
Log.e(tag, "shouldShowWirelessChargingWarningTip()", e);
}
return false;
}
/** Stores the state of whether the wireless charging warning in Settings is enabled. */
public static void updateWirelessChargingWarningEnabled(
@NonNull Context context, boolean enabled, @NonNull String tag) {
try {
Secure.putInt(
context.getContentResolver(),
WIRELESS_CHARGING_WARNING_ENABLED,
enabled ? 1 : 0);
} catch (Exception e) {
Log.e(tag, "setWirelessChargingWarningEnabled()", e);
}
}
private static boolean shouldShowWirelessChargingNotificationInternal(
@NonNull Context context, @NonNull String tag) {
final long lastNotificationTimeMillis =
Secure.getLong(
context.getContentResolver(),
WIRELESS_CHARGING_NOTIFICATION_TIMESTAMP,
WIRELESS_CHARGING_DEFAULT_TIMESTAMP);
if (isWirelessChargingNotificationDisabled(lastNotificationTimeMillis)) {
return false;
}
if (isInitialWirelessChargingNotification(lastNotificationTimeMillis)) {
updateWirelessChargingNotificationTimestamp(context, System.currentTimeMillis(), tag);
updateWirelessChargingWarningEnabled(context, /* enabled= */ true, tag);
return true;
}
final long durationMillis = System.currentTimeMillis() - lastNotificationTimeMillis;
final boolean show = durationMillis > WIRELESS_CHARGING_NOTIFICATION_THRESHOLD_MILLIS;
Log.d(tag, "shouldShowWirelessChargingNotification = " + show);
if (show) {
updateWirelessChargingNotificationTimestamp(context, System.currentTimeMillis(), tag);
updateWirelessChargingWarningEnabled(context, /* enabled= */ true, tag);
}
return show;
}
private static boolean isWirelessChargingNotificationDisabled(long lastNotificationTimeMillis) {
return lastNotificationTimeMillis == Long.MIN_VALUE;
}
private static boolean isInitialWirelessChargingNotification(long lastNotificationTimeMillis) {
return lastNotificationTimeMillis == WIRELESS_CHARGING_DEFAULT_TIMESTAMP;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib;
import android.content.Context;
import android.provider.Settings;
public class WirelessUtils {
public static boolean isRadioAllowed(Context context, String type) {
if (!isAirplaneModeOn(context)) {
return true;
}
String toggleable = Settings.Global.getString(context.getContentResolver(),
Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS);
return toggleable != null && toggleable.contains(type);
}
public static boolean isAirplaneModeOn(Context context) {
return Settings.Global.getInt(context.getContentResolver(),
Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.accessibility;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.Context;
import android.provider.Settings;
import android.view.accessibility.AccessibilityManager;
import java.util.List;
/**
* A helper class to assist determining the state of the accessibility button that can appear
* within the software-rendered navigation area.
*/
public class AccessibilityButtonHelper {
public static boolean isRequestedByMagnification(Context ctx) {
return Settings.Secure.getInt(ctx.getContentResolver(),
Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED, 0) == 1;
}
public static boolean isRequestedByAccessibilityService(Context ctx) {
final AccessibilityManager accessibilityManager = ctx.getSystemService(
AccessibilityManager.class);
List<AccessibilityServiceInfo> services =
accessibilityManager.getEnabledAccessibilityServiceList(
AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
if (services != null) {
for (int i = 0, size = services.size(); i < size; i++) {
if ((services.get(i).flags
& AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON)
!= 0) {
return true;
}
}
}
return false;
}
public static boolean isRequested(Context ctx) {
return isRequestedByMagnification(ctx) || isRequestedByAccessibilityService(ctx);
}
}

View File

@@ -0,0 +1,213 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.settingslib.accessibility;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArraySet;
import android.view.accessibility.AccessibilityManager;
import com.android.internal.R;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
public class AccessibilityUtils {
public static final char ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR = ':';
/**
* @return the set of enabled accessibility services. If there are no services,
* it returns the unmodifiable {@link Collections#emptySet()}.
*/
public static Set<ComponentName> getEnabledServicesFromSettings(Context context) {
return getEnabledServicesFromSettings(context, UserHandle.myUserId());
}
/**
* Check if the accessibility service is crashed
*
* @param packageName The package name to check
* @param serviceName The service name to check
* @param installedServiceInfos The list of installed accessibility service
* @return {@code true} if the accessibility service is crashed for the user.
* {@code false} otherwise.
*/
public static boolean hasServiceCrashed(String packageName, String serviceName,
List<AccessibilityServiceInfo> installedServiceInfos) {
for (int i = 0; i < installedServiceInfos.size(); i++) {
final AccessibilityServiceInfo accessibilityServiceInfo = installedServiceInfos.get(i);
final ServiceInfo serviceInfo =
installedServiceInfos.get(i).getResolveInfo().serviceInfo;
if (TextUtils.equals(serviceInfo.packageName, packageName)
&& TextUtils.equals(serviceInfo.name, serviceName)) {
return accessibilityServiceInfo.crashed;
}
}
return false;
}
/**
* @return the set of enabled accessibility services for {@param userId}. If there are no
* services, it returns the unmodifiable {@link Collections#emptySet()}.
*/
public static Set<ComponentName> getEnabledServicesFromSettings(Context context, int userId) {
final String enabledServicesSetting = Settings.Secure.getStringForUser(
context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
userId);
if (TextUtils.isEmpty(enabledServicesSetting)) {
return Collections.emptySet();
}
final Set<ComponentName> enabledServices = new HashSet<>();
final TextUtils.StringSplitter colonSplitter =
new TextUtils.SimpleStringSplitter(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR);
colonSplitter.setString(enabledServicesSetting);
for (String componentNameString : colonSplitter) {
final ComponentName enabledService = ComponentName.unflattenFromString(
componentNameString);
if (enabledService != null) {
enabledServices.add(enabledService);
}
}
return enabledServices;
}
/**
* @return a localized version of the text resource specified by resId
*/
public static CharSequence getTextForLocale(Context context, Locale locale, int resId) {
final Resources res = context.getResources();
final Configuration config = new Configuration(res.getConfiguration());
config.setLocale(locale);
final Context langContext = context.createConfigurationContext(config);
return langContext.getText(resId);
}
/**
* Changes an accessibility component's state.
*/
public static void setAccessibilityServiceState(Context context, ComponentName toggledService,
boolean enabled) {
setAccessibilityServiceState(context, toggledService, enabled, UserHandle.myUserId());
}
/**
* Changes an accessibility component's state for {@param userId}.
*/
public static void setAccessibilityServiceState(Context context, ComponentName toggledService,
boolean enabled, int userId) {
// Parse the enabled services.
Set<ComponentName> enabledServices = AccessibilityUtils.getEnabledServicesFromSettings(
context, userId);
if (enabledServices.isEmpty()) {
enabledServices = new ArraySet<>(1);
}
// Determine enabled services and accessibility state.
boolean accessibilityEnabled = false;
if (enabled) {
enabledServices.add(toggledService);
// Enabling at least one service enables accessibility.
accessibilityEnabled = true;
} else {
enabledServices.remove(toggledService);
// Check how many enabled and installed services are present.
Set<ComponentName> installedServices = getInstalledServices(context);
for (ComponentName enabledService : enabledServices) {
if (installedServices.contains(enabledService)) {
// Disabling the last service disables accessibility.
accessibilityEnabled = true;
break;
}
}
}
// Update the enabled services setting.
StringBuilder enabledServicesBuilder = new StringBuilder();
// Keep the enabled services even if they are not installed since we
// have no way to know whether the application restore process has
// completed. In general the system should be responsible for the
// clean up not settings.
for (ComponentName enabledService : enabledServices) {
enabledServicesBuilder.append(enabledService.flattenToString());
enabledServicesBuilder.append(
AccessibilityUtils.ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR);
}
final int enabledServicesBuilderLength = enabledServicesBuilder.length();
if (enabledServicesBuilderLength > 0) {
enabledServicesBuilder.deleteCharAt(enabledServicesBuilderLength - 1);
}
Settings.Secure.putStringForUser(context.getContentResolver(),
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
enabledServicesBuilder.toString(), userId);
}
/**
* Get the name of the service that should be toggled by the accessibility shortcut. Use
* an OEM-configurable default if the setting has never been set.
*
* @param context A valid context
* @param userId The user whose settings should be checked
* @return The component name, flattened to a string, of the target service.
*/
public static String getShortcutTargetServiceComponentNameString(
Context context, int userId) {
final String currentShortcutServiceId = Settings.Secure.getStringForUser(
context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
userId);
if (currentShortcutServiceId != null) {
return currentShortcutServiceId;
}
return context.getString(R.string.config_defaultAccessibilityService);
}
private static Set<ComponentName> getInstalledServices(Context context) {
final Set<ComponentName> installedServices = new HashSet<>();
installedServices.clear();
final List<AccessibilityServiceInfo> installedServiceInfos =
AccessibilityManager.getInstance(context)
.getInstalledAccessibilityServiceList();
if (installedServiceInfos == null) {
return installedServices;
}
for (final AccessibilityServiceInfo info : installedServiceInfos) {
final ResolveInfo resolveInfo = info.getResolveInfo();
final ComponentName installedService = new ComponentName(
resolveInfo.serviceInfo.packageName,
resolveInfo.serviceInfo.name);
installedServices.add(installedService);
}
return installedServices;
}
}

View File

@@ -0,0 +1,271 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.settingslib.accounts;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorDescription;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SyncAdapterType;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.os.UserHandle;
import android.util.Log;
import com.android.settingslib.Utils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* Helper class for monitoring accounts on the device for a given user.
*
* Classes using this helper should implement {@link OnAccountsUpdateListener}.
* {@link OnAccountsUpdateListener#onAccountsUpdate(UserHandle)} will then be
* called once accounts get updated. For setting up listening for account
* updates, {@link #listenToAccountUpdates()} and
* {@link #stopListeningToAccountUpdates()} should be used.
*/
final public class AuthenticatorHelper extends BroadcastReceiver {
private static final String TAG = "AuthenticatorHelper";
private final Map<String, AuthenticatorDescription> mTypeToAuthDescription = new HashMap<>();
private final ArrayList<String> mEnabledAccountTypes = new ArrayList<>();
private final Map<String, Drawable> mAccTypeIconCache = new HashMap<>();
private final HashMap<String, ArrayList<String>> mAccountTypeToAuthorities = new HashMap<>();
private final UserHandle mUserHandle;
private final Context mContext;
private final OnAccountsUpdateListener mListener;
private boolean mListeningToAccountUpdates;
public interface OnAccountsUpdateListener {
void onAccountsUpdate(UserHandle userHandle);
}
public AuthenticatorHelper(Context context, UserHandle userHandle,
OnAccountsUpdateListener listener) {
mContext = context;
mUserHandle = userHandle;
mListener = listener;
// This guarantees that the helper is ready to use once constructed: the account types and
// authorities are initialized
onAccountsUpdated(null);
}
public String[] getEnabledAccountTypes() {
return mEnabledAccountTypes.toArray(new String[mEnabledAccountTypes.size()]);
}
public void preloadDrawableForType(final Context context, final String accountType) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
getDrawableForType(context, accountType);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
}
/**
* Gets an icon associated with a particular account type. If none found, return null.
* @param accountType the type of account
* @return a drawable for the icon or a default icon returned by
* {@link PackageManager#getDefaultActivityIcon} if one cannot be found.
*/
public Drawable getDrawableForType(Context context, final String accountType) {
Drawable icon = null;
synchronized (mAccTypeIconCache) {
if (mAccTypeIconCache.containsKey(accountType)) {
return mAccTypeIconCache.get(accountType);
}
}
if (mTypeToAuthDescription.containsKey(accountType)) {
try {
AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
Context authContext = context.createPackageContextAsUser(desc.packageName, 0,
mUserHandle);
icon = mContext.getPackageManager().getUserBadgedIcon(
authContext.getDrawable(desc.iconId), mUserHandle);
synchronized (mAccTypeIconCache) {
mAccTypeIconCache.put(accountType, icon);
}
} catch (PackageManager.NameNotFoundException|Resources.NotFoundException e) {
// Ignore
}
}
if (icon == null) {
icon = context.getPackageManager().getDefaultActivityIcon();
}
return Utils.getBadgedIcon(mContext, icon, mUserHandle);
}
/**
* Gets the label associated with a particular account type. If none found, return null.
* @param accountType the type of account
* @return a CharSequence for the label or null if one cannot be found.
*/
public CharSequence getLabelForType(Context context, final String accountType) {
CharSequence label = null;
if (mTypeToAuthDescription.containsKey(accountType)) {
try {
AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
Context authContext = context.createPackageContextAsUser(desc.packageName, 0,
mUserHandle);
label = authContext.getResources().getText(desc.labelId);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "No label name for account type " + accountType);
} catch (Resources.NotFoundException e) {
Log.w(TAG, "No label icon for account type " + accountType);
}
}
return label;
}
/**
* Gets the package associated with a particular account type. If none found, return null.
* @param accountType the type of account
* @return the package name or null if one cannot be found.
*/
public String getPackageForType(final String accountType) {
if (mTypeToAuthDescription.containsKey(accountType)) {
AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
return desc.packageName;
}
return null;
}
/**
* Gets the resource id of the label associated with a particular account type. If none found,
* return -1.
* @param accountType the type of account
* @return a resource id for the label or -1 if none found;
*/
public int getLabelIdForType(final String accountType) {
if (mTypeToAuthDescription.containsKey(accountType)) {
AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
return desc.labelId;
}
return -1;
}
/**
* Updates provider icons. Subclasses should call this in onCreate()
* and update any UI that depends on AuthenticatorDescriptions in onAuthDescriptionsUpdated().
*/
public void updateAuthDescriptions(Context context) {
AuthenticatorDescription[] authDescs = AccountManager.get(context)
.getAuthenticatorTypesAsUser(mUserHandle.getIdentifier());
for (int i = 0; i < authDescs.length; i++) {
mTypeToAuthDescription.put(authDescs[i].type, authDescs[i]);
}
}
public boolean containsAccountType(String accountType) {
return mTypeToAuthDescription.containsKey(accountType);
}
public AuthenticatorDescription getAccountTypeDescription(String accountType) {
return mTypeToAuthDescription.get(accountType);
}
public boolean hasAccountPreferences(final String accountType) {
if (containsAccountType(accountType)) {
AuthenticatorDescription desc = getAccountTypeDescription(accountType);
if (desc != null && desc.accountPreferencesId != 0) {
return true;
}
}
return false;
}
void onAccountsUpdated(Account[] accounts) {
updateAuthDescriptions(mContext);
if (accounts == null) {
accounts = AccountManager.get(mContext).getAccountsAsUser(mUserHandle.getIdentifier());
}
mEnabledAccountTypes.clear();
mAccTypeIconCache.clear();
for (int i = 0; i < accounts.length; i++) {
final Account account = accounts[i];
if (!mEnabledAccountTypes.contains(account.type)) {
mEnabledAccountTypes.add(account.type);
}
}
buildAccountTypeToAuthoritiesMap();
if (mListeningToAccountUpdates) {
mListener.onAccountsUpdate(mUserHandle);
}
}
@Override
public void onReceive(final Context context, final Intent intent) {
// TODO: watch for package upgrades to invalidate cache; see http://b/7206643
final Account[] accounts = AccountManager.get(mContext)
.getAccountsAsUser(mUserHandle.getIdentifier());
onAccountsUpdated(accounts);
}
public void listenToAccountUpdates() {
if (!mListeningToAccountUpdates) {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION);
// At disk full, certain actions are blocked (such as writing the accounts to storage).
// It is useful to also listen for recovery from disk full to avoid bugs.
intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
mContext.registerReceiverAsUser(this, mUserHandle, intentFilter, null, null);
mListeningToAccountUpdates = true;
}
}
public void stopListeningToAccountUpdates() {
if (mListeningToAccountUpdates) {
mContext.unregisterReceiver(this);
mListeningToAccountUpdates = false;
}
}
public ArrayList<String> getAuthoritiesForAccountType(String type) {
return mAccountTypeToAuthorities.get(type);
}
private void buildAccountTypeToAuthoritiesMap() {
mAccountTypeToAuthorities.clear();
SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(
mUserHandle.getIdentifier());
for (int i = 0, n = syncAdapters.length; i < n; i++) {
final SyncAdapterType sa = syncAdapters[i];
ArrayList<String> authorities = mAccountTypeToAuthorities.get(sa.accountType);
if (authorities == null) {
authorities = new ArrayList<String>();
mAccountTypeToAuthorities.put(sa.accountType, authorities);
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Added authority " + sa.authority + " to accountType "
+ sa.accountType);
}
authorities.add(sa.authority);
}
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.settingslib.animation;
import android.animation.AnimatorListenerAdapter;
import android.view.animation.Interpolator;
/**
* An interface which can create animations when starting an appear animation with
* {@link AppearAnimationUtils}
*/
public interface AppearAnimationCreator<T> {
/**
* Create the appear / disappear animation.
*/
void createAnimation(T animatedObject, long delay, long duration,
float translationY, boolean appearing, Interpolator interpolator,
Runnable endRunnable);
/**
* Create the animation with {@link AnimatorListenerAdapter}.
*/
default void createAnimation(T animatedObject, long delay, long duration,
float translationY, boolean appearing, Interpolator interpolator,
Runnable endRunnable, AnimatorListenerAdapter animatorListener) {}
}

View File

@@ -0,0 +1,280 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.settingslib.animation;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.view.RenderNodeAnimator;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import com.android.settingslib.R;
/**
* A class to make nice appear transitions for views in a tabular layout.
*/
public class AppearAnimationUtils implements AppearAnimationCreator<View> {
public static final long DEFAULT_APPEAR_DURATION = 220;
private final Interpolator mInterpolator;
private final float mStartTranslation;
private final AppearAnimationProperties mProperties = new AppearAnimationProperties();
protected final float mDelayScale;
private final long mDuration;
protected RowTranslationScaler mRowTranslationScaler;
protected boolean mAppearing;
public AppearAnimationUtils(Context ctx) {
this(ctx, DEFAULT_APPEAR_DURATION,
1.0f, 1.0f,
AnimationUtils.loadInterpolator(ctx, android.R.interpolator.linear_out_slow_in));
}
public AppearAnimationUtils(Context ctx, long duration, float translationScaleFactor,
float delayScaleFactor, Interpolator interpolator) {
mInterpolator = interpolator;
mStartTranslation = ctx.getResources().getDimensionPixelOffset(
R.dimen.appear_y_translation_start) * translationScaleFactor;
mDelayScale = delayScaleFactor;
mDuration = duration;
mAppearing = true;
}
public void startAnimation2d(View[][] objects, final Runnable finishListener) {
startAnimation2d(objects, finishListener, this);
}
public void startAnimation(View[] objects, final Runnable finishListener) {
startAnimation(objects, finishListener, this);
}
public <T> void startAnimation2d(T[][] objects, final Runnable finishListener,
AppearAnimationCreator<T> creator) {
AppearAnimationProperties properties = getDelays(objects);
startAnimations(properties, objects, finishListener, creator);
}
public <T> void startAnimation(T[] objects, final Runnable finishListener,
AppearAnimationCreator<T> creator) {
AppearAnimationProperties properties = getDelays(objects);
startAnimations(properties, objects, finishListener, creator);
}
private <T> void startAnimations(AppearAnimationProperties properties, T[] objects,
final Runnable finishListener, AppearAnimationCreator<T> creator) {
if (properties.maxDelayRowIndex == -1 || properties.maxDelayColIndex == -1) {
finishListener.run();
return;
}
for (int row = 0; row < properties.delays.length; row++) {
long[] columns = properties.delays[row];
long delay = columns[0];
Runnable endRunnable = null;
if (properties.maxDelayRowIndex == row && properties.maxDelayColIndex == 0) {
endRunnable = finishListener;
}
float translationScale = mRowTranslationScaler != null
? mRowTranslationScaler.getRowTranslationScale(row, properties.delays.length)
: 1f;
float translation = translationScale * mStartTranslation;
creator.createAnimation(objects[row], delay, mDuration,
mAppearing ? translation : -translation,
mAppearing, mInterpolator, endRunnable);
}
}
private <T> void startAnimations(AppearAnimationProperties properties, T[][] objects,
final Runnable finishListener, AppearAnimationCreator<T> creator) {
if (properties.maxDelayRowIndex == -1 || properties.maxDelayColIndex == -1) {
finishListener.run();
return;
}
for (int row = 0; row < properties.delays.length; row++) {
long[] columns = properties.delays[row];
float translationScale = mRowTranslationScaler != null
? mRowTranslationScaler.getRowTranslationScale(row, properties.delays.length)
: 1f;
float translation = translationScale * mStartTranslation;
for (int col = 0; col < columns.length; col++) {
long delay = columns[col];
Runnable endRunnable = null;
if (properties.maxDelayRowIndex == row && properties.maxDelayColIndex == col) {
endRunnable = finishListener;
}
creator.createAnimation(objects[row][col], delay, mDuration,
mAppearing ? translation : -translation,
mAppearing, mInterpolator, endRunnable);
}
}
}
private <T> AppearAnimationProperties getDelays(T[] items) {
long maxDelay = -1;
mProperties.maxDelayColIndex = -1;
mProperties.maxDelayRowIndex = -1;
mProperties.delays = new long[items.length][];
for (int row = 0; row < items.length; row++) {
mProperties.delays[row] = new long[1];
long delay = calculateDelay(row, 0);
mProperties.delays[row][0] = delay;
if (items[row] != null && delay > maxDelay) {
maxDelay = delay;
mProperties.maxDelayColIndex = 0;
mProperties.maxDelayRowIndex = row;
}
}
return mProperties;
}
private <T> AppearAnimationProperties getDelays(T[][] items) {
long maxDelay = -1;
mProperties.maxDelayColIndex = -1;
mProperties.maxDelayRowIndex = -1;
mProperties.delays = new long[items.length][];
for (int row = 0; row < items.length; row++) {
T[] columns = items[row];
mProperties.delays[row] = new long[columns.length];
for (int col = 0; col < columns.length; col++) {
long delay = calculateDelay(row, col);
mProperties.delays[row][col] = delay;
if (items[row][col] != null && delay > maxDelay) {
maxDelay = delay;
mProperties.maxDelayColIndex = col;
mProperties.maxDelayRowIndex = row;
}
}
}
return mProperties;
}
protected long calculateDelay(int row, int col) {
return (long) ((row * 40 + col * (Math.pow(row, 0.4) + 0.4) * 20) * mDelayScale);
}
public Interpolator getInterpolator() {
return mInterpolator;
}
public float getStartTranslation() {
return mStartTranslation;
}
@Override
public void createAnimation(final View view, long delay, long duration, float translationY,
boolean appearing, Interpolator interpolator, final Runnable endRunnable) {
createAnimation(
view, delay, duration, translationY, appearing, interpolator, endRunnable, null);
}
@Override
public void createAnimation(final View view, long delay,
long duration, float translationY, boolean appearing, Interpolator interpolator,
final Runnable endRunnable, final AnimatorListenerAdapter animatorListener) {
if (view != null) {
float targetAlpha = appearing ? 1f : 0f;
float targetTranslationY = appearing ? 0 : translationY;
view.setAlpha(1.0f - targetAlpha);
view.setTranslationY(translationY - targetTranslationY);
Animator alphaAnim;
if (view.isHardwareAccelerated()) {
RenderNodeAnimator alphaAnimRt = new RenderNodeAnimator(RenderNodeAnimator.ALPHA,
targetAlpha);
alphaAnimRt.setTarget(view);
alphaAnim = alphaAnimRt;
} else {
alphaAnim = ObjectAnimator.ofFloat(view, View.ALPHA, view.getAlpha(), targetAlpha);
}
alphaAnim.setInterpolator(interpolator);
alphaAnim.setDuration(duration);
alphaAnim.setStartDelay(delay);
if (view.hasOverlappingRendering()) {
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
alphaAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setLayerType(View.LAYER_TYPE_NONE, null);
}
});
}
alphaAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setAlpha(targetAlpha);
if (endRunnable != null) {
endRunnable.run();
}
}
});
alphaAnim.start();
startTranslationYAnimation(view, delay, duration, targetTranslationY,
interpolator, animatorListener);
}
}
/**
* A static method to start translation y animation
*/
public static void startTranslationYAnimation(View view, long delay, long duration,
float endTranslationY, Interpolator interpolator) {
startTranslationYAnimation(view, delay, duration, endTranslationY, interpolator, null);
}
/**
* A static method to start translation y animation
*/
public static void startTranslationYAnimation(View view, long delay, long duration,
float endTranslationY, Interpolator interpolator, AnimatorListenerAdapter listener) {
Animator translationAnim;
if (view.isHardwareAccelerated()) {
RenderNodeAnimator translationAnimRt = new RenderNodeAnimator(
RenderNodeAnimator.TRANSLATION_Y, endTranslationY);
translationAnimRt.setTarget(view);
translationAnim = translationAnimRt;
} else {
translationAnim = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y,
view.getTranslationY(), endTranslationY);
}
translationAnim.setInterpolator(interpolator);
translationAnim.setDuration(duration);
translationAnim.setStartDelay(delay);
if (listener != null) {
translationAnim.addListener(listener);
}
translationAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setTranslationY(endTranslationY);
}
});
translationAnim.start();
}
public class AppearAnimationProperties {
public long[][] delays;
public int maxDelayRowIndex;
public int maxDelayColIndex;
}
public interface RowTranslationScaler {
float getRowTranslationScale(int row, int numRows);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.settingslib.animation;
import android.content.Context;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
/**
* A class to make nice disappear transitions for views in a tabular layout.
*/
public class DisappearAnimationUtils extends AppearAnimationUtils {
public DisappearAnimationUtils(Context ctx) {
this(ctx, DEFAULT_APPEAR_DURATION,
1.0f, 1.0f,
AnimationUtils.loadInterpolator(ctx, android.R.interpolator.fast_out_linear_in));
}
public DisappearAnimationUtils(Context ctx, long duration, float translationScaleFactor,
float delayScaleFactor, Interpolator interpolator) {
this(ctx, duration, translationScaleFactor, delayScaleFactor, interpolator,
ROW_TRANSLATION_SCALER);
}
public DisappearAnimationUtils(Context ctx, long duration, float translationScaleFactor,
float delayScaleFactor, Interpolator interpolator, RowTranslationScaler rowScaler) {
super(ctx, duration, translationScaleFactor, delayScaleFactor, interpolator);
mRowTranslationScaler = rowScaler;
mAppearing = false;
}
protected long calculateDelay(int row, int col) {
return (long) ((row * 60 + col * (Math.pow(row, 0.4) + 0.4) * 10) * mDelayScale);
}
private static final RowTranslationScaler ROW_TRANSLATION_SCALER = new RowTranslationScaler() {
@Override
public float getRowTranslationScale(int row, int numRows) {
return (float) (Math.pow((numRows - row), 2) / numRows);
}
};
}

View File

@@ -0,0 +1,136 @@
/*
* 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.settingslib.applications;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.util.Log;
import android.util.LruCache;
import androidx.annotation.VisibleForTesting;
/**
* Cache app icon for management.
*/
public class AppIconCacheManager {
private static final String TAG = "AppIconCacheManager";
private static final float CACHE_RATIO = 0.1f;
@VisibleForTesting
static final int MAX_CACHE_SIZE_IN_KB = getMaxCacheInKb();
private static final String DELIMITER = ":";
private static AppIconCacheManager sAppIconCacheManager;
private final LruCache<String, Drawable> mDrawableCache;
private AppIconCacheManager() {
mDrawableCache = new LruCache<String, Drawable>(MAX_CACHE_SIZE_IN_KB) {
@Override
protected int sizeOf(String key, Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap().getByteCount() / 1024;
}
// Rough estimate each pixel will use 4 bytes by default.
return drawable.getIntrinsicHeight() * drawable.getIntrinsicWidth() * 4 / 1024;
}
};
}
/**
* Get an {@link AppIconCacheManager} instance.
*/
public static synchronized AppIconCacheManager getInstance() {
if (sAppIconCacheManager == null) {
sAppIconCacheManager = new AppIconCacheManager();
}
return sAppIconCacheManager;
}
/**
* Put app icon to cache
*
* @param packageName of icon
* @param uid of packageName
* @param drawable app icon
*/
public void put(String packageName, int uid, Drawable drawable) {
final String key = getKey(packageName, uid);
if (key == null || drawable == null || drawable.getIntrinsicHeight() < 0
|| drawable.getIntrinsicWidth() < 0) {
Log.w(TAG, "Invalid key or drawable.");
return;
}
mDrawableCache.put(key, drawable);
}
/**
* Get app icon from cache.
*
* @param packageName of icon
* @param uid of packageName
* @return app icon
*/
public Drawable get(String packageName, int uid) {
final String key = getKey(packageName, uid);
if (key == null) {
Log.w(TAG, "Invalid key with package or uid.");
return null;
}
final Drawable cachedDrawable = mDrawableCache.get(key);
return cachedDrawable != null ? cachedDrawable.mutate() : null;
}
/**
* Release cache.
*/
public static void release() {
if (sAppIconCacheManager != null) {
sAppIconCacheManager.mDrawableCache.evictAll();
}
}
private static String getKey(String packageName, int uid) {
if (packageName == null || uid < 0) {
return null;
}
return packageName + DELIMITER + UserHandle.getUserId(uid);
}
private static int getMaxCacheInKb() {
return Math.round(CACHE_RATIO * Runtime.getRuntime().maxMemory() / 1024);
}
/**
* Clears as much memory as possible.
*
* @see android.content.ComponentCallbacks2#onTrimMemory(int)
*/
public void trimMemory(int level) {
if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// Time to clear everything
if (sAppIconCacheManager != null) {
sAppIconCacheManager.mDrawableCache.trimToSize(0);
}
} else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
|| level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
// Tough time but still affordable, clear half of the cache
if (sAppIconCacheManager != null) {
final int maxSize = sAppIconCacheManager.mDrawableCache.maxSize();
sAppIconCacheManager.mDrawableCache.trimToSize(maxSize / 2);
}
}
}
}

View File

@@ -0,0 +1,328 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.settingslib.applications;
import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.Flags;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.hardware.usb.IUsbManager;
import android.net.Uri;
import android.os.Environment;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.Log;
import com.android.settingslib.R;
import com.android.settingslib.Utils;
import com.android.settingslib.applications.instantapps.InstantAppDataProvider;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.List;
public class AppUtils {
private static final String TAG = "AppUtils";
/**
* This should normally only be set in robolectric tests, to avoid getting a method not found
* exception when calling the isInstantApp method of the ApplicationInfo class, because
* robolectric does not yet have an implementation of it.
*/
private static InstantAppDataProvider sInstantAppDataProvider = null;
private static final Intent sBrowserIntent;
static {
sBrowserIntent = new Intent()
.setAction(Intent.ACTION_VIEW)
.addCategory(Intent.CATEGORY_BROWSABLE)
.setData(Uri.parse("http:"));
}
public static CharSequence getLaunchByDefaultSummary(ApplicationsState.AppEntry appEntry,
IUsbManager usbManager, PackageManager pm, Context context) {
String packageName = appEntry.info.packageName;
boolean hasPreferred = hasPreferredActivities(pm, packageName)
|| hasUsbDefaults(usbManager, packageName);
int status = pm.getIntentVerificationStatusAsUser(packageName, UserHandle.myUserId());
// consider a visible current link-handling state to be any explicitly designated behavior
boolean hasDomainURLsPreference =
status != PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED;
return context.getString(hasPreferred || hasDomainURLsPreference
? R.string.launch_defaults_some
: R.string.launch_defaults_none);
}
public static boolean hasUsbDefaults(IUsbManager usbManager, String packageName) {
try {
if (usbManager != null) {
return usbManager.hasDefaults(packageName, UserHandle.myUserId());
}
} catch (RemoteException e) {
Log.e(TAG, "mUsbManager.hasDefaults", e);
}
return false;
}
public static boolean hasPreferredActivities(PackageManager pm, String packageName) {
// Get list of preferred activities
List<ComponentName> prefActList = new ArrayList<>();
// Intent list cannot be null. so pass empty list
List<IntentFilter> intentList = new ArrayList<>();
pm.getPreferredActivities(intentList, prefActList, packageName);
Log.d(TAG, "Have " + prefActList.size() + " number of activities in preferred list");
return prefActList.size() > 0;
}
/**
* Returns a boolean indicating whether the given package should be considered an instant app
*/
public static boolean isInstant(ApplicationInfo info) {
if (sInstantAppDataProvider != null) {
if (sInstantAppDataProvider.isInstantApp(info)) {
return true;
}
} else if (info.isInstantApp()) {
return true;
}
// For debugging/testing, we support setting the following property to a comma-separated
// list of search terms (typically, but not necessarily, full package names) to match
// against the package names of the app.
String propVal = SystemProperties.get("settingsdebug.instant.packages");
if (propVal != null && !propVal.isEmpty() && info.packageName != null) {
String[] searchTerms = propVal.split(",");
if (searchTerms != null) {
for (String term : searchTerms) {
if (info.packageName.contains(term)) {
return true;
}
}
}
}
return false;
}
/** Returns the label for a given package. */
public static CharSequence getApplicationLabel(
PackageManager packageManager, String packageName) {
return com.android.settingslib.utils.applications.AppUtils
.getApplicationLabel(packageManager, packageName);
}
/**
* Returns a boolean indicating whether the given package is a hidden system module
*/
public static boolean isHiddenSystemModule(Context context, String packageName) {
return ApplicationsState.getInstance((Application) context.getApplicationContext())
.isHiddenModule(packageName);
}
/**
* Returns a boolean indicating whether a given package is a system module.
*/
public static boolean isSystemModule(Context context, String packageName) {
return ApplicationsState.getInstance((Application) context.getApplicationContext())
.isSystemModule(packageName);
}
/**
* Returns a boolean indicating whether a given package is a mainline module.
*/
public static boolean isMainlineModule(PackageManager pm, String packageName) {
// Check if the package is listed among the system modules.
try {
pm.getModuleInfo(packageName, 0 /* flags */);
return true;
} catch (PackageManager.NameNotFoundException e) {
//pass
}
try {
final PackageInfo pkg = pm.getPackageInfo(packageName, 0 /* flags */);
if (Flags.provideInfoOfApkInApex()) {
return pkg.getApexPackageName() != null;
} else {
// Check if the package is contained in an APEX. There is no public API to properly
// check whether a given APK package comes from an APEX registered as module.
// Therefore we conservatively assume that any package scanned from an /apex path is
// a system package.
return pkg.applicationInfo.sourceDir.startsWith(
Environment.getApexDirectory().getAbsolutePath());
}
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
/**
* Returns a content description of an app name which distinguishes a personal app from a
* work app for accessibility purpose.
* If the app is in a work profile, then add a "work" prefix to the app name.
*/
public static String getAppContentDescription(Context context, String packageName,
int userId) {
return com.android.settingslib.utils.applications.AppUtils.getAppContentDescription(context,
packageName, userId);
}
/**
* Returns a boolean indicating whether a given package is a browser app.
*
* An app is a "browser" if it has an activity resolution that wound up
* marked with the 'handleAllWebDataURI' flag.
*/
public static boolean isBrowserApp(Context context, String packageName, int userId) {
sBrowserIntent.setPackage(packageName);
final List<ResolveInfo> list = context.getPackageManager().queryIntentActivitiesAsUser(
sBrowserIntent, PackageManager.MATCH_ALL, userId);
for (ResolveInfo info : list) {
if (info.activityInfo != null && info.handleAllWebDataURI) {
return true;
}
}
return false;
}
/**
* Returns a boolean indicating whether a given package is a default browser.
*
* @param packageName a given package.
* @return true if the given package is default browser.
*/
public static boolean isDefaultBrowser(Context context, String packageName) {
final String defaultBrowserPackage =
context.getPackageManager().getDefaultBrowserPackageNameAsUser(
UserHandle.myUserId());
return TextUtils.equals(packageName, defaultBrowserPackage);
}
/**
* Get the app icon by app entry.
*
* @param context caller's context
* @param appEntry AppEntry of ApplicationsState
* @return app icon of the app entry
*/
public static Drawable getIcon(Context context, ApplicationsState.AppEntry appEntry) {
if (appEntry == null || appEntry.info == null) {
return null;
}
final AppIconCacheManager appIconCacheManager = AppIconCacheManager.getInstance();
final String packageName = appEntry.info.packageName;
final int uid = appEntry.info.uid;
Drawable icon = appIconCacheManager.get(packageName, uid);
if (icon == null) {
if (appEntry.apkFile != null && appEntry.apkFile.exists()) {
icon = Utils.getBadgedIcon(context, appEntry.info);
appIconCacheManager.put(packageName, uid, icon);
} else {
setAppEntryMounted(appEntry, /* mounted= */ false);
icon = context.getDrawable(
com.android.internal.R.drawable.sym_app_on_sd_unavailable_icon);
}
} else if (!appEntry.mounted && appEntry.apkFile != null && appEntry.apkFile.exists()) {
// If the app wasn't mounted but is now mounted, reload its icon.
setAppEntryMounted(appEntry, /* mounted= */ true);
icon = Utils.getBadgedIcon(context, appEntry.info);
appIconCacheManager.put(packageName, uid, icon);
}
return icon;
}
/**
* Get the app icon from cache by app entry.
*
* @param appEntry AppEntry of ApplicationsState
* @return app icon of the app entry
*/
public static Drawable getIconFromCache(ApplicationsState.AppEntry appEntry) {
return appEntry == null || appEntry.info == null ? null
: AppIconCacheManager.getInstance().get(
appEntry.info.packageName,
appEntry.info.uid);
}
/**
* Preload the top N icons of app entry list.
*
* @param context caller's context
* @param appEntries AppEntry list of ApplicationsState
* @param number the number of Top N icons of the appEntries
*/
public static void preloadTopIcons(Context context,
ArrayList<ApplicationsState.AppEntry> appEntries, int number) {
if (appEntries == null || appEntries.isEmpty() || number <= 0) {
return;
}
for (int i = 0; i < Math.min(appEntries.size(), number); i++) {
final ApplicationsState.AppEntry entry = appEntries.get(i);
var unused = ThreadUtils.getBackgroundExecutor().submit(() -> {
getIcon(context, entry);
});
}
}
/**
* Returns a boolean indicating whether this app is installed or not.
*
* @param appEntry AppEntry of ApplicationsState.
* @return true if the app is in installed state.
*/
public static boolean isAppInstalled(ApplicationsState.AppEntry appEntry) {
if (appEntry == null || appEntry.info == null) {
return false;
}
return (appEntry.info.flags & ApplicationInfo.FLAG_INSTALLED) != 0;
}
private static void setAppEntryMounted(ApplicationsState.AppEntry appEntry, boolean mounted) {
if (appEntry.mounted != mounted) {
synchronized (appEntry) {
appEntry.mounted = mounted;
}
}
}
/**
* Returns clone user profile id if present. Returns -1 if not present.
*/
public static int getCloneUserId(Context context) {
UserManager userManager = context.getSystemService(UserManager.class);
for (UserHandle userHandle : userManager.getUserProfiles()) {
if (userManager.getUserInfo(userHandle.getIdentifier()).isCloneProfile()) {
return userHandle.getIdentifier();
}
}
return -1;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.applications;
import android.app.AppGlobals;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.RemoteException;
import android.util.IconDrawableFactory;
import androidx.annotation.Nullable;
import com.android.settingslib.widget.CandidateInfo;
/**
* Data model representing an app in DefaultAppPicker UI.
*/
public class DefaultAppInfo extends CandidateInfo {
public final int userId;
public final ComponentName componentName;
public final PackageItemInfo packageItemInfo;
public final String summary;
protected final PackageManager mPm;
private final Context mContext;
public DefaultAppInfo(Context context, PackageManager pm, int uid, ComponentName cn) {
this(context, pm, uid, cn, null /* summary */, true /* enabled */);
}
public DefaultAppInfo(Context context, PackageManager pm, int uid, PackageItemInfo info) {
this(context, pm, uid, info, null /* summary */, true /* enabled */);
}
public DefaultAppInfo(Context context, PackageManager pm, int uid, ComponentName cn,
String summary, boolean enabled) {
super(enabled);
mContext = context;
mPm = pm;
packageItemInfo = null;
userId = uid;
componentName = cn;
this.summary = summary;
}
public DefaultAppInfo(Context context, PackageManager pm, int uid, PackageItemInfo info,
String summary, boolean enabled) {
super(enabled);
mContext = context;
mPm = pm;
userId = uid;
packageItemInfo = info;
componentName = null;
this.summary = summary;
}
@Override
public CharSequence loadLabel() {
if (componentName != null) {
try {
final ComponentInfo componentInfo = getComponentInfo();
if (componentInfo != null) {
return componentInfo.loadLabel(mPm);
} else {
final ApplicationInfo appInfo = mPm.getApplicationInfoAsUser(
componentName.getPackageName(), 0, userId);
return appInfo.loadLabel(mPm);
}
} catch (PackageManager.NameNotFoundException e) {
return null;
}
} else if (packageItemInfo != null) {
return packageItemInfo.loadLabel(mPm);
} else {
return null;
}
}
public @Nullable String getSummary() {
return this.summary;
}
@Override
public Drawable loadIcon() {
final IconDrawableFactory factory = IconDrawableFactory.newInstance(mContext);
if (componentName != null) {
try {
final ComponentInfo componentInfo = getComponentInfo();
final ApplicationInfo appInfo = mPm.getApplicationInfoAsUser(
componentName.getPackageName(), 0, userId);
if (componentInfo != null) {
return factory.getBadgedIcon(componentInfo, appInfo, userId);
} else {
return factory.getBadgedIcon(appInfo);
}
} catch (PackageManager.NameNotFoundException e) {
return null;
}
}
if (packageItemInfo != null) {
try {
final ApplicationInfo appInfo = mPm.getApplicationInfoAsUser(
packageItemInfo.packageName, 0, userId);
return factory.getBadgedIcon(packageItemInfo, appInfo, userId);
} catch (PackageManager.NameNotFoundException e) {
return null;
}
} else {
return null;
}
}
@Override
public String getKey() {
if (componentName != null) {
return componentName.flattenToString();
} else if (packageItemInfo != null) {
return packageItemInfo.packageName;
} else {
return null;
}
}
private ComponentInfo getComponentInfo() {
try {
ComponentInfo componentInfo = AppGlobals.getPackageManager().getActivityInfo(
componentName, 0, userId);
if (componentInfo == null) {
componentInfo = AppGlobals.getPackageManager().getServiceInfo(
componentName, 0, userId);
}
return componentInfo;
} catch (RemoteException e) {
return null;
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.settingslib.applications;
import android.annotation.SuppressLint;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import androidx.annotation.NonNull;
/**
* A class for applying config changes and determing if doing so resulting in any "interesting"
* changes.
*/
public class InterestingConfigChanges {
private final Configuration mLastConfiguration = new Configuration();
private final int mFlags;
public InterestingConfigChanges() {
this(ActivityInfo.CONFIG_LOCALE | ActivityInfo.CONFIG_LAYOUT_DIRECTION
| ActivityInfo.CONFIG_UI_MODE | ActivityInfo.CONFIG_ASSETS_PATHS
| ActivityInfo.CONFIG_DENSITY);
}
public InterestingConfigChanges(int flags) {
mFlags = flags;
}
/**
* Applies the given config change and returns whether an "interesting" change happened.
*
* @param res The source of the new config to apply
*
* @return Whether interesting changes occurred
*/
@SuppressLint("NewApi")
public boolean applyNewConfig(Resources res) {
return applyNewConfig(res.getConfiguration());
}
/**
* Applies the given config change and returns whether an "interesting" change happened.
*/
public boolean applyNewConfig(@NonNull Configuration configuration) {
int configChanges = mLastConfiguration.updateFrom(
Configuration.generateDelta(mLastConfiguration, configuration));
return (configChanges & (mFlags)) != 0;
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.applications;
import android.content.Context;
import android.permission.PermissionControllerManager;
import android.permission.RuntimePermissionPresentationInfo;
import java.text.Collator;
import java.util.ArrayList;
import java.util.List;
/**
* Helper to get the runtime permissions for an app.
*/
public class PermissionsSummaryHelper {
public static void getPermissionSummary(Context context, String pkg,
final PermissionsResultCallback callback) {
final PermissionControllerManager permController =
context.getSystemService(PermissionControllerManager.class);
permController.getAppPermissions(pkg, permissions -> {
int grantedAdditionalCount = 0;
int requestedCount = 0;
List<CharSequence> grantedStandardLabels = new ArrayList<>();
for (RuntimePermissionPresentationInfo permission : permissions) {
requestedCount++;
if (permission.isGranted()) {
if (permission.isStandard()) {
grantedStandardLabels.add(permission.getLabel());
} else {
grantedAdditionalCount++;
}
}
}
Collator collator = Collator.getInstance();
collator.setStrength(Collator.PRIMARY);
grantedStandardLabels.sort(collator);
callback.onPermissionSummaryResult(
requestedCount, grantedAdditionalCount, grantedStandardLabels);
}, null);
}
/**
* Callback for the runtime permissions result for an app.
*/
public interface PermissionsResultCallback {
/** The runtime permission summary result for an app. */
void onPermissionSummaryResult(
int requestedPermissionCount, int additionalGrantedPermissionCount,
List<CharSequence> grantedGroupLabels);
}
}

View File

@@ -0,0 +1,274 @@
/*
* Copyright (C) 2021 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.settingslib.applications;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.PermissionChecker;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.os.UserManager;
import android.permission.PermissionManager;
import android.text.format.DateUtils;
import android.util.IconDrawableFactory;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Retrieval of app ops information for the specified ops.
*/
public class RecentAppOpsAccess {
@VisibleForTesting
static final int[] LOCATION_OPS = new int[]{
AppOpsManager.OP_FINE_LOCATION,
AppOpsManager.OP_COARSE_LOCATION,
};
private static final int[] MICROPHONE_OPS = new int[]{
AppOpsManager.OP_RECORD_AUDIO,
};
private static final int[] CAMERA_OPS = new int[]{
AppOpsManager.OP_CAMERA,
};
private static final String TAG = RecentAppOpsAccess.class.getSimpleName();
@VisibleForTesting
public static final String ANDROID_SYSTEM_PACKAGE_NAME = "android";
// Keep last 24 hours of access app information.
private static final long RECENT_TIME_INTERVAL_MILLIS = DateUtils.DAY_IN_MILLIS;
/** The flags for querying ops that are trusted for showing in the UI. */
public static final int TRUSTED_STATE_FLAGS = AppOpsManager.OP_FLAG_SELF
| AppOpsManager.OP_FLAG_UNTRUSTED_PROXY
| AppOpsManager.OP_FLAG_TRUSTED_PROXIED;
private final PackageManager mPackageManager;
private final Context mContext;
private final int[] mOps;
private final IconDrawableFactory mDrawableFactory;
private final Clock mClock;
public RecentAppOpsAccess(Context context, int[] ops) {
this(context, Clock.systemDefaultZone(), ops);
}
@VisibleForTesting
RecentAppOpsAccess(Context context, Clock clock, int[] ops) {
mContext = context;
mPackageManager = context.getPackageManager();
mOps = ops;
mDrawableFactory = IconDrawableFactory.newInstance(context);
mClock = clock;
}
/**
* Creates an instance of {@link RecentAppOpsAccess} for location (coarse and fine) access.
*/
public static RecentAppOpsAccess createForLocation(Context context) {
return new RecentAppOpsAccess(context, LOCATION_OPS);
}
/**
* Creates an instance of {@link RecentAppOpsAccess} for microphone access.
*/
public static RecentAppOpsAccess createForMicrophone(Context context) {
return new RecentAppOpsAccess(context, MICROPHONE_OPS);
}
/**
* Creates an instance of {@link RecentAppOpsAccess} for camera access.
*/
public static RecentAppOpsAccess createForCamera(Context context) {
return new RecentAppOpsAccess(context, CAMERA_OPS);
}
/**
* Fills a list of applications which queried for access recently within specified time.
* Apps are sorted by recency. Apps with more recent accesses are in the front.
*/
@VisibleForTesting
public List<Access> getAppList(boolean showSystemApps) {
// Retrieve a access usage list from AppOps
AppOpsManager aoManager = mContext.getSystemService(AppOpsManager.class);
List<AppOpsManager.PackageOps> appOps = aoManager.getPackagesForOps(mOps);
final int appOpsCount = appOps != null ? appOps.size() : 0;
// Process the AppOps list and generate a preference list.
ArrayList<Access> accesses = new ArrayList<>(appOpsCount);
final long now = mClock.millis();
final UserManager um = mContext.getSystemService(UserManager.class);
final List<UserHandle> profiles = um.getUserProfiles();
for (int i = 0; i < appOpsCount; ++i) {
AppOpsManager.PackageOps ops = appOps.get(i);
String packageName = ops.getPackageName();
int uid = ops.getUid();
UserHandle user = UserHandle.getUserHandleForUid(uid);
// Don't show apps belonging to background users except managed users.
if (!profiles.contains(user)) {
continue;
}
// Don't show apps that do not have user sensitive location permissions
boolean showApp = true;
if (!showSystemApps) {
for (int op : mOps) {
final String permission = AppOpsManager.opToPermission(op);
final int permissionFlags = mPackageManager.getPermissionFlags(permission,
packageName,
user);
if (PermissionChecker.checkPermissionForPreflight(mContext, permission,
PermissionChecker.PID_UNKNOWN, uid, packageName)
== PermissionChecker.PERMISSION_GRANTED) {
if ((permissionFlags
& PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED)
== 0) {
showApp = false;
break;
}
} else {
if ((permissionFlags
& PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED) == 0) {
showApp = false;
break;
}
}
}
}
if (showApp && PermissionManager.shouldShowPackageForIndicatorCached(mContext,
packageName)) {
Access access = getAccessFromOps(now, ops);
if (access != null) {
accesses.add(access);
}
}
}
return accesses;
}
/**
* Gets a list of apps that accessed the app op recently, sorting by recency.
*
* @param showSystemApps whether includes system apps in the list.
* @return the list of apps that recently accessed the app op.
*/
public List<Access> getAppListSorted(boolean showSystemApps) {
List<Access> accesses = getAppList(showSystemApps);
// Sort the list of Access by recency. Most recent accesses first.
Collections.sort(accesses, Collections.reverseOrder(new Comparator<Access>() {
@Override
public int compare(Access access1, Access access2) {
return Long.compare(access1.accessFinishTime, access2.accessFinishTime);
}
}));
return accesses;
}
/**
* Creates a Access entry for the given PackageOps.
*
* This method examines the time interval of the PackageOps first. If the PackageOps is older
* than the designated interval, this method ignores the PackageOps object and returns null.
* When the PackageOps is fresh enough, this method returns a Access object for the package
*/
private Access getAccessFromOps(long now,
AppOpsManager.PackageOps ops) {
String packageName = ops.getPackageName();
List<AppOpsManager.OpEntry> entries = ops.getOps();
long accessFinishTime = 0L;
// Earliest time for a access to end and still be shown in list.
long recentAccessCutoffTime = now - RECENT_TIME_INTERVAL_MILLIS;
// Compute the most recent access time from all op entries.
for (AppOpsManager.OpEntry entry : entries) {
long lastAccessTime = entry.getLastAccessTime(TRUSTED_STATE_FLAGS);
if (lastAccessTime > accessFinishTime) {
accessFinishTime = lastAccessTime;
}
}
// Bail out if the entry is out of date.
if (accessFinishTime < recentAccessCutoffTime) {
return null;
}
// The package is fresh enough, continue.
int uid = ops.getUid();
int userId = UserHandle.getUserId(uid);
Access access = null;
try {
ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser(
packageName, PackageManager.GET_META_DATA, userId);
if (appInfo == null) {
Log.w(TAG, "Null application info retrieved for package " + packageName
+ ", userId " + userId);
return null;
}
final UserHandle userHandle = new UserHandle(userId);
Drawable icon = mDrawableFactory.getBadgedIcon(appInfo, userId);
CharSequence appLabel = mPackageManager.getApplicationLabel(appInfo);
CharSequence badgedAppLabel = mPackageManager.getUserBadgedLabel(appLabel, userHandle);
if (appLabel.toString().contentEquals(badgedAppLabel)) {
// If badged label is not different from original then no need for it as
// a separate content description.
badgedAppLabel = null;
}
access = new Access(packageName, userHandle, icon, appLabel, badgedAppLabel,
accessFinishTime);
} catch (NameNotFoundException e) {
Log.w(TAG, "package name not found for " + packageName + ", userId " + userId);
}
return access;
}
/**
* Information about when an app last accessed a particular app op.
*/
public static class Access {
public final String packageName;
public final UserHandle userHandle;
public final Drawable icon;
public final CharSequence label;
public final CharSequence contentDescription;
public final long accessFinishTime;
public Access(String packageName, UserHandle userHandle, Drawable icon,
CharSequence label, CharSequence contentDescription,
long accessFinishTime) {
this.packageName = packageName;
this.userHandle = userHandle;
this.icon = icon;
this.label = label;
this.contentDescription = contentDescription;
this.accessFinishTime = accessFinishTime;
}
}
}

View File

@@ -0,0 +1,253 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.applications;
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
import android.provider.Settings;
import android.util.Slog;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.function.Predicate;
/**
* Class for managing services matching a given intent and requesting a given permission.
*/
public class ServiceListing {
private final ContentResolver mContentResolver;
private final Context mContext;
private final String mTag;
private final String mSetting;
private final String mIntentAction;
private final String mPermission;
private final String mNoun;
private final boolean mAddDeviceLockedFlags;
private final HashSet<ComponentName> mEnabledServices = new HashSet<>();
private final List<ServiceInfo> mServices = new ArrayList<>();
private final List<Callback> mCallbacks = new ArrayList<>();
private final Predicate mValidator;
private boolean mListening;
private ServiceListing(Context context, String tag,
String setting, String intentAction, String permission, String noun,
boolean addDeviceLockedFlags, Predicate validator) {
mContentResolver = context.getContentResolver();
mContext = context;
mTag = tag;
mSetting = setting;
mIntentAction = intentAction;
mPermission = permission;
mNoun = noun;
mAddDeviceLockedFlags = addDeviceLockedFlags;
mValidator = validator;
}
public void addCallback(Callback callback) {
mCallbacks.add(callback);
}
public void removeCallback(Callback callback) {
mCallbacks.remove(callback);
}
public void setListening(boolean listening) {
if (mListening == listening) return;
mListening = listening;
if (mListening) {
// listen for package changes
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
filter.addDataScheme("package");
mContext.registerReceiver(mPackageReceiver, filter);
mContentResolver.registerContentObserver(Settings.Secure.getUriFor(mSetting),
false, mSettingsObserver);
} else {
mContext.unregisterReceiver(mPackageReceiver);
mContentResolver.unregisterContentObserver(mSettingsObserver);
}
}
private void saveEnabledServices() {
StringBuilder sb = null;
for (ComponentName cn : mEnabledServices) {
if (sb == null) {
sb = new StringBuilder();
} else {
sb.append(':');
}
sb.append(cn.flattenToString());
}
Settings.Secure.putString(mContentResolver, mSetting,
sb != null ? sb.toString() : "");
}
private void loadEnabledServices() {
mEnabledServices.clear();
final String flat = Settings.Secure.getString(mContentResolver, mSetting);
if (flat != null && !"".equals(flat)) {
final String[] names = flat.split(":");
for (String name : names) {
final ComponentName cn = ComponentName.unflattenFromString(name);
if (cn != null) {
mEnabledServices.add(cn);
}
}
}
}
public void reload() {
loadEnabledServices();
mServices.clear();
final int user = ActivityManager.getCurrentUser();
int flags = PackageManager.GET_SERVICES | PackageManager.GET_META_DATA;
if (mAddDeviceLockedFlags) {
flags |= PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
}
final PackageManager pmWrapper = mContext.getPackageManager();
List<ResolveInfo> installedServices = pmWrapper.queryIntentServicesAsUser(
new Intent(mIntentAction), flags, user);
for (ResolveInfo resolveInfo : installedServices) {
ServiceInfo info = resolveInfo.serviceInfo;
if (!mPermission.equals(info.permission)) {
Slog.w(mTag, "Skipping " + mNoun + " service "
+ info.packageName + "/" + info.name
+ ": it does not require the permission "
+ mPermission);
continue;
}
if (mValidator != null && !mValidator.test(info)) {
continue;
}
mServices.add(info);
}
for (Callback callback : mCallbacks) {
callback.onServicesReloaded(mServices);
}
}
public boolean isEnabled(ComponentName cn) {
return mEnabledServices.contains(cn);
}
public void setEnabled(ComponentName cn, boolean enabled) {
if (enabled) {
mEnabledServices.add(cn);
} else {
mEnabledServices.remove(cn);
}
saveEnabledServices();
}
private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange, Uri uri) {
reload();
}
};
private final BroadcastReceiver mPackageReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
reload();
}
};
public interface Callback {
void onServicesReloaded(List<ServiceInfo> services);
}
public static class Builder {
private final Context mContext;
private String mTag;
private String mSetting;
private String mIntentAction;
private String mPermission;
private String mNoun;
private boolean mAddDeviceLockedFlags = false;
private Predicate mValidator;
public Builder(Context context) {
mContext = context;
}
public Builder setTag(String tag) {
mTag = tag;
return this;
}
public Builder setSetting(String setting) {
mSetting = setting;
return this;
}
public Builder setIntentAction(String intentAction) {
mIntentAction = intentAction;
return this;
}
public Builder setPermission(String permission) {
mPermission = permission;
return this;
}
public Builder setNoun(String noun) {
mNoun = noun;
return this;
}
public Builder setValidator(Predicate<ServiceInfo> validator) {
mValidator = validator;
return this;
}
/**
* Set to true to add support for both MATCH_DIRECT_BOOT_AWARE and
* MATCH_DIRECT_BOOT_UNAWARE flags when querying PackageManager. Required to get results
* prior to the user unlocking the device for the first time.
*/
public Builder setAddDeviceLockedFlags(boolean addDeviceLockedFlags) {
mAddDeviceLockedFlags = addDeviceLockedFlags;
return this;
}
public ServiceListing build() {
return new ServiceListing(mContext, mTag, mSetting, mIntentAction, mPermission, mNoun,
mAddDeviceLockedFlags, mValidator);
}
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.settingslib.applications;
import android.app.usage.StorageStats;
import android.app.usage.StorageStatsManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import androidx.annotation.VisibleForTesting;
import java.io.IOException;
/**
* StorageStatsSource wraps the StorageStatsManager for testability purposes.
*/
public class StorageStatsSource {
private StorageStatsManager mStorageStatsManager;
public StorageStatsSource(Context context) {
mStorageStatsManager = context.getSystemService(StorageStatsManager.class);
}
public StorageStatsSource.ExternalStorageStats getExternalStorageStats(String volumeUuid,
UserHandle user) throws IOException {
return new StorageStatsSource.ExternalStorageStats(
mStorageStatsManager.queryExternalStatsForUser(volumeUuid, user));
}
public StorageStatsSource.AppStorageStats getStatsForUid(String volumeUuid, int uid)
throws IOException {
return new StorageStatsSource.AppStorageStatsImpl(
mStorageStatsManager.queryStatsForUid(volumeUuid, uid));
}
public StorageStatsSource.AppStorageStats getStatsForPackage(
String volumeUuid, String packageName, UserHandle user)
throws PackageManager.NameNotFoundException, IOException {
return new StorageStatsSource.AppStorageStatsImpl(
mStorageStatsManager.queryStatsForPackage(volumeUuid, packageName, user));
}
public long getCacheQuotaBytes(String volumeUuid, int uid) {
return mStorageStatsManager.getCacheQuotaBytes(volumeUuid, uid);
}
/**
* Static class that provides methods for querying the amount of external storage available as
* well as breaking it up into several media types.
*/
public static class ExternalStorageStats {
public long totalBytes;
public long audioBytes;
public long videoBytes;
public long imageBytes;
public long appBytes;
/** Convenience method for testing. */
@VisibleForTesting
public ExternalStorageStats(
long totalBytes, long audioBytes, long videoBytes, long imageBytes, long appBytes) {
this.totalBytes = totalBytes;
this.audioBytes = audioBytes;
this.videoBytes = videoBytes;
this.imageBytes = imageBytes;
this.appBytes = appBytes;
}
/**
* Creates an ExternalStorageStats from the system version of ExternalStorageStats. They are
* identical other than the utility method created for test purposes.
* @param stats The stats to copy to wrap.
*/
public ExternalStorageStats(android.app.usage.ExternalStorageStats stats) {
totalBytes = stats.getTotalBytes();
audioBytes = stats.getAudioBytes();
videoBytes = stats.getVideoBytes();
imageBytes = stats.getImageBytes();
appBytes = stats.getAppBytes();
}
}
/**
* Interface that exists to simplify testing. The platform {@link StorageStats} is too new and
* robolectric cannot see it. It simply wraps a StorageStats object and forwards method calls
* to the real object
*/
public interface AppStorageStats {
long getCodeBytes();
long getDataBytes();
long getCacheBytes();
long getTotalBytes();
}
/**
* Simple implementation of AppStorageStats that will allow you to query the StorageStats object
* passed in for storage information about an app.
*/
public static class AppStorageStatsImpl implements
StorageStatsSource.AppStorageStats {
private StorageStats mStats;
public AppStorageStatsImpl(StorageStats stats) {
mStats = stats;
}
public long getCodeBytes() {
return mStats.getAppBytes();
}
public long getDataBytes() {
return mStats.getDataBytes();
}
public long getCacheBytes() {
return mStats.getCacheBytes();
}
public long getTotalBytes() {
return mStats.getAppBytes() + mStats.getDataBytes();
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.applications.instantapps;
import android.content.pm.ApplicationInfo;
/**
* This helps deal with the fact that robolectric does not yet have an implementation of the
* isInstantApp method of ApplicationInfo, so we get a method not found exception when running tests
* if we try to call it directly.
*/
public interface InstantAppDataProvider {
public boolean isInstantApp(ApplicationInfo info);
}

View File

@@ -0,0 +1,389 @@
/*
* 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_AUDIO;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCodecConfig;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.Build;
import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class A2dpProfile implements LocalBluetoothProfile {
private static final String TAG = "A2dpProfile";
private Context mContext;
private BluetoothA2dp mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
private final BluetoothAdapter mBluetoothAdapter;
static final ParcelUuid[] SINK_UUIDS = {
BluetoothUuid.A2DP_SINK,
BluetoothUuid.ADV_AUDIO_DIST,
};
static final String NAME = "A2DP";
private final LocalBluetoothProfileManager mProfileManager;
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
// These callbacks run on the main thread.
private final class A2dpServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothA2dp) proxy;
// We just bound to the service, so refresh the UI for any connected A2DP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "A2dpProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(A2dpProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady = true;
mProfileManager.callServiceConnectedListeners();
}
public void onServiceDisconnected(int profile) {
mIsProfileReady = false;
mProfileManager.callServiceDisconnectedListeners();
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.A2DP;
}
A2dpProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mContext = context;
mDeviceManager = deviceManager;
mProfileManager = profileManager;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mBluetoothAdapter.getProfileProxy(context, new A2dpServiceListener(),
BluetoothProfile.A2DP);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
/**
* Get A2dp devices matching connection states{
* @code BluetoothProfile.STATE_CONNECTED,
* @code BluetoothProfile.STATE_CONNECTING,
* @code BluetoothProfile.STATE_DISCONNECTING}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectedDevices() {
return getDevicesByStates(new int[] {
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
/**
* Get A2dp devices matching connection states{
* @code BluetoothProfile.STATE_DISCONNECTED,
* @code BluetoothProfile.STATE_CONNECTED,
* @code BluetoothProfile.STATE_CONNECTING,
* @code BluetoothProfile.STATE_DISCONNECTING}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectableDevices() {
return getDevicesByStates(new int[] {
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
private List<BluetoothDevice> getDevicesByStates(int[] states) {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(states);
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
return device == null
? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_AUDIO)
: mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_AUDIO);
}
public BluetoothDevice getActiveDevice() {
if (mBluetoothAdapter == null) return null;
final List<BluetoothDevice> activeDevices = mBluetoothAdapter
.getActiveDevices(BluetoothProfile.A2DP);
return (activeDevices.size() > 0) ? activeDevices.get(0) : null;
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
boolean isA2dpPlaying() {
if (mService == null) return false;
List<BluetoothDevice> sinks = mService.getConnectedDevices();
for (BluetoothDevice device : sinks) {
if (mService.isA2dpPlaying(device)) {
return true;
}
}
return false;
}
public boolean supportsHighQualityAudio(BluetoothDevice device) {
BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
if (bluetoothDevice == null) {
return false;
}
int support = mService.isOptionalCodecsSupported(bluetoothDevice);
return support == BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED;
}
/**
* @return whether high quality audio is enabled or not
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public boolean isHighQualityAudioEnabled(BluetoothDevice device) {
BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
if (bluetoothDevice == null) {
return false;
}
int enabled = mService.isOptionalCodecsEnabled(bluetoothDevice);
if (enabled != BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN) {
return enabled == BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED;
} else if (getConnectionStatus(bluetoothDevice) != BluetoothProfile.STATE_CONNECTED
&& supportsHighQualityAudio(bluetoothDevice)) {
// Since we don't have a stored preference and the device isn't connected, just return
// true since the default behavior when the device gets connected in the future would be
// to have optional codecs enabled.
return true;
}
BluetoothCodecConfig codecConfig = null;
if (mService.getCodecStatus(bluetoothDevice) != null) {
codecConfig = mService.getCodecStatus(bluetoothDevice).getCodecConfig();
}
if (codecConfig != null) {
return !codecConfig.isMandatoryCodec();
} else {
return false;
}
}
public void setHighQualityAudioEnabled(BluetoothDevice device, boolean enabled) {
BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
if (bluetoothDevice == null) {
return;
}
int prefValue = enabled
? BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED
: BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED;
mService.setOptionalCodecsEnabled(bluetoothDevice, prefValue);
if (getConnectionStatus(bluetoothDevice) != BluetoothProfile.STATE_CONNECTED) {
return;
}
if (enabled) {
mService.enableOptionalCodecs(bluetoothDevice);
} else {
mService.disableOptionalCodecs(bluetoothDevice);
}
}
/**
* Gets the label associated with the codec of a Bluetooth device.
*
* @param device to get codec label from
* @return the label associated with the device codec
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public String getHighQualityAudioOptionLabel(BluetoothDevice device) {
BluetoothDevice bluetoothDevice = (device != null) ? device : getActiveDevice();
int unknownCodecId = R.string.bluetooth_profile_a2dp_high_quality_unknown_codec;
if (bluetoothDevice == null || !supportsHighQualityAudio(device)
|| getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED) {
return mContext.getString(unknownCodecId);
}
// We want to get the highest priority codec, since that's the one that will be used with
// this device, and see if it is high-quality (ie non-mandatory).
List<BluetoothCodecConfig> selectable = null;
if (mService.getCodecStatus(device) != null) {
selectable = mService.getCodecStatus(device).getCodecsSelectableCapabilities();
// To get the highest priority, we sort in reverse.
Collections.sort(selectable,
(a, b) -> {
return b.getCodecPriority() - a.getCodecPriority();
});
}
final BluetoothCodecConfig codecConfig = (selectable == null || selectable.size() < 1)
? null : selectable.get(0);
final int codecType = (codecConfig == null || codecConfig.isMandatoryCodec())
? BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID : codecConfig.getCodecType();
int index = -1;
switch (codecType) {
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC:
index = 1;
break;
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC:
index = 2;
break;
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX:
index = 3;
break;
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_APTX_HD:
index = 4;
break;
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_LDAC:
index = 5;
break;
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_LC3:
index = 6;
break;
case BluetoothCodecConfig.SOURCE_CODEC_TYPE_OPUS:
index = 7;
break;
}
if (index < 0) {
return mContext.getString(unknownCodecId);
}
return mContext.getString(R.string.bluetooth_profile_a2dp_high_quality,
mContext.getResources().getStringArray(R.array.bluetooth_a2dp_codec_titles)[index]);
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_a2dp;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_a2dp_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_a2dp_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_headphones_a2dp;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.A2DP,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up A2DP proxy", t);
}
}
}
}

View File

@@ -0,0 +1,214 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothA2dpSink;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
final class A2dpSinkProfile implements LocalBluetoothProfile {
private static final String TAG = "A2dpSinkProfile";
private BluetoothA2dpSink mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
static final ParcelUuid[] SRC_UUIDS = {
BluetoothUuid.A2DP_SOURCE,
BluetoothUuid.ADV_AUDIO_DIST,
};
static final String NAME = "A2DPSink";
private final LocalBluetoothProfileManager mProfileManager;
// Order of this profile in device profiles list
private static final int ORDINAL = 5;
// These callbacks run on the main thread.
private final class A2dpSinkServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothA2dpSink) proxy;
// We just bound to the service, so refresh the UI for any connected A2DP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "A2dpSinkProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(A2dpSinkProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady=true;
}
public void onServiceDisconnected(int profile) {
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.A2DP_SINK;
}
A2dpSinkProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new A2dpSinkServiceListener(),
BluetoothProfile.A2DP_SINK);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
boolean isAudioPlaying() {
if (mService == null) {
return false;
}
List<BluetoothDevice> srcs = mService.getConnectedDevices();
if (!srcs.isEmpty()) {
if (mService.isAudioPlaying(srcs.get(0))) {
return true;
}
}
return false;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
// we need to have same string in UI for even SINK Media Audio.
return R.string.bluetooth_profile_a2dp;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_a2dp_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_a2dp_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_headphones_a2dp;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.A2DP_SINK,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up A2DP proxy", t);
}
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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.settingslib.bluetooth;
public final class BluetoothBroadcastUtils {
/**
* The fragment tag specified to FragmentManager for container activities to manage fragments.
*/
public static final String TAG_FRAGMENT_QR_CODE_SCANNER = "qr_code_scanner_fragment";
/**
* Action for launching qr code scanner activity.
*/
public static final String ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER =
"android.settings.BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER";
/**
* Extra for {@link android.bluetooth.BluetoothDevice}.
*/
public static final String EXTRA_BLUETOOTH_DEVICE_SINK = "bluetooth_device_sink";
/**
* Extra for checking the {@link android.bluetooth.BluetoothLeBroadcastAssistant} should perform
* this operation for all coordinated set members throughout one session or not.
*/
public static final String EXTRA_BLUETOOTH_SINK_IS_GROUP = "bluetooth_sink_is_group";
/**
* Bluetooth scheme.
*/
public static final String SCHEME_BT_BROADCAST_METADATA = "BLUETOOTH:UUID:184F;";
}

View File

@@ -0,0 +1,183 @@
/*
* 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothAdapter.STATE_CONNECTED;
import static android.bluetooth.BluetoothAdapter.STATE_CONNECTING;
import static android.bluetooth.BluetoothAdapter.STATE_DISCONNECTED;
import static android.bluetooth.BluetoothAdapter.STATE_DISCONNECTING;
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static android.bluetooth.BluetoothAdapter.STATE_TURNING_OFF;
import static android.bluetooth.BluetoothAdapter.STATE_TURNING_ON;
import android.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* BluetoothCallback provides a callback interface for the settings
* UI to receive events from {@link BluetoothEventManager}.
*/
public interface BluetoothCallback {
/**
* It will be called when the state of the local Bluetooth adapter has been changed.
* It is listening {@link android.bluetooth.BluetoothAdapter#ACTION_STATE_CHANGED}.
* For example, Bluetooth has been turned on or off.
*
* @param bluetoothState the current Bluetooth state, the possible values are:
* {@link android.bluetooth.BluetoothAdapter#STATE_OFF},
* {@link android.bluetooth.BluetoothAdapter#STATE_TURNING_ON},
* {@link android.bluetooth.BluetoothAdapter#STATE_ON},
* {@link android.bluetooth.BluetoothAdapter#STATE_TURNING_OFF}.
*/
default void onBluetoothStateChanged(@AdapterState int bluetoothState) {}
/**
* It will be called when the local Bluetooth adapter has started
* or finished the remote device discovery process.
* It is listening {@link android.bluetooth.BluetoothAdapter#ACTION_DISCOVERY_STARTED} and
* {@link android.bluetooth.BluetoothAdapter#ACTION_DISCOVERY_FINISHED}.
*
* @param started indicate the current process is started or finished.
*/
default void onScanningStateChanged(boolean started) {}
/**
* It will be called in following situations:
* 1. In scanning mode, when a new device has been found.
* 2. When a profile service is connected and existing connected devices has been found.
* This API only invoked once for each device and all devices will be cached in
* {@link CachedBluetoothDeviceManager}.
*
* @param cachedDevice the Bluetooth device.
*/
default void onDeviceAdded(@NonNull CachedBluetoothDevice cachedDevice) {}
/**
* It will be called when requiring to remove a remote device from CachedBluetoothDevice list
*
* @param cachedDevice the Bluetooth device.
*/
default void onDeviceDeleted(@NonNull CachedBluetoothDevice cachedDevice) {}
/**
* It will be called when bond state of a remote device is changed.
* It is listening {@link android.bluetooth.BluetoothDevice#ACTION_BOND_STATE_CHANGED}
*
* @param cachedDevice the Bluetooth device.
* @param bondState the Bluetooth device bond state, the possible values are:
* {@link android.bluetooth.BluetoothDevice#BOND_NONE},
* {@link android.bluetooth.BluetoothDevice#BOND_BONDING},
* {@link android.bluetooth.BluetoothDevice#BOND_BONDED}.
*/
default void onDeviceBondStateChanged(
@NonNull CachedBluetoothDevice cachedDevice, int bondState) {}
/**
* It will be called in following situations:
* 1. When the adapter is not connected to any profiles of any remote devices
* and it attempts a connection to a profile.
* 2. When the adapter disconnects from the last profile of the last device.
* It is listening {@link android.bluetooth.BluetoothAdapter#ACTION_CONNECTION_STATE_CHANGED}
*
* @param cachedDevice the Bluetooth device.
* @param state the Bluetooth device connection state, the possible values are:
* {@link android.bluetooth.BluetoothAdapter#STATE_DISCONNECTED},
* {@link android.bluetooth.BluetoothAdapter#STATE_CONNECTING},
* {@link android.bluetooth.BluetoothAdapter#STATE_CONNECTED},
* {@link android.bluetooth.BluetoothAdapter#STATE_DISCONNECTING}.
*/
default void onConnectionStateChanged(
@Nullable CachedBluetoothDevice cachedDevice,
@ConnectionState int state) {}
/**
* It will be called when device been set as active for {@code bluetoothProfile}
* It is listening in following intent:
* {@link android.bluetooth.BluetoothA2dp#ACTION_ACTIVE_DEVICE_CHANGED}
* {@link android.bluetooth.BluetoothHeadset#ACTION_ACTIVE_DEVICE_CHANGED}
* {@link android.bluetooth.BluetoothHearingAid#ACTION_ACTIVE_DEVICE_CHANGED}
*
* @param activeDevice the active Bluetooth device.
* @param bluetoothProfile the profile of active Bluetooth device.
*/
default void onActiveDeviceChanged(
@Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {}
/**
* It will be called in following situations:
* 1. When the call state on the device is changed.
* 2. When the audio connection state of the A2DP profile is changed.
* It is listening in following intent:
* {@link android.bluetooth.BluetoothHeadset#ACTION_AUDIO_STATE_CHANGED}
* {@link android.telephony.TelephonyManager#ACTION_PHONE_STATE_CHANGED}
*/
default void onAudioModeChanged() {}
/**
* It will be called when one of the bluetooth device profile connection state is changed.
*
* @param cachedDevice the active Bluetooth device.
* @param state the BluetoothProfile connection state, the possible values are:
* {@link android.bluetooth.BluetoothProfile#STATE_CONNECTED},
* {@link android.bluetooth.BluetoothProfile#STATE_CONNECTING},
* {@link android.bluetooth.BluetoothProfile#STATE_DISCONNECTED},
* {@link android.bluetooth.BluetoothProfile#STATE_DISCONNECTING}.
* @param bluetoothProfile the BluetoothProfile id.
*/
default void onProfileConnectionStateChanged(
@NonNull CachedBluetoothDevice cachedDevice,
@ConnectionState int state,
int bluetoothProfile) {
}
/**
* Called when ACL connection state is changed. It listens to
* {@link android.bluetooth.BluetoothDevice#ACTION_ACL_CONNECTED} and {@link
* android.bluetooth.BluetoothDevice#ACTION_ACL_DISCONNECTED}
*
* @param cachedDevice Bluetooth device that changed
* @param state the Bluetooth device connection state, the possible values are:
* {@link android.bluetooth.BluetoothAdapter#STATE_DISCONNECTED},
* {@link android.bluetooth.BluetoothAdapter#STATE_CONNECTED}
*/
default void onAclConnectionStateChanged(
@NonNull CachedBluetoothDevice cachedDevice, int state) {}
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "STATE_" }, value = {
STATE_DISCONNECTED,
STATE_CONNECTING,
STATE_CONNECTED,
STATE_DISCONNECTING,
})
@interface ConnectionState {}
@IntDef(prefix = { "STATE_" }, value = {
STATE_OFF,
STATE_TURNING_ON,
STATE_ON,
STATE_TURNING_OFF,
})
@Retention(RetentionPolicy.SOURCE)
@interface AdapterState {}
}

View File

@@ -0,0 +1,177 @@
/*
* 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.settingslib.bluetooth;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothUuid;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.internal.util.ArrayUtils;
/**
* BluetoothDeviceFilter contains a static method that returns a
* Filter object that returns whether or not the BluetoothDevice
* passed to it matches the specified filter type constant from
* {@link android.bluetooth.BluetoothDevicePicker}.
*/
public final class BluetoothDeviceFilter {
private static final String TAG = "BluetoothDeviceFilter";
/** The filter interface to external classes. */
public interface Filter {
boolean matches(BluetoothDevice device);
}
/** All filter singleton (referenced directly). */
public static final Filter ALL_FILTER = new AllFilter();
/** Bonded devices only filter (referenced directly). */
public static final Filter BONDED_DEVICE_FILTER = new BondedDeviceFilter();
/** Unbonded devices only filter (referenced directly). */
public static final Filter UNBONDED_DEVICE_FILTER = new UnbondedDeviceFilter();
/** Table of singleton filter objects. */
private static final Filter[] FILTERS = {
ALL_FILTER, // FILTER_TYPE_ALL
new AudioFilter(), // FILTER_TYPE_AUDIO
new TransferFilter(), // FILTER_TYPE_TRANSFER
new PanuFilter(), // FILTER_TYPE_PANU
new NapFilter() // FILTER_TYPE_NAP
};
/** Private constructor. */
private BluetoothDeviceFilter() {
}
/**
* Returns the singleton {@link Filter} object for the specified type,
* or {@link #ALL_FILTER} if the type value is out of range.
*
* @param filterType a constant from BluetoothDevicePicker
* @return a singleton object implementing the {@link Filter} interface.
*/
public static Filter getFilter(int filterType) {
if (filterType >= 0 && filterType < FILTERS.length) {
return FILTERS[filterType];
} else {
Log.w(TAG, "Invalid filter type " + filterType + " for device picker");
return ALL_FILTER;
}
}
/** Filter that matches all devices. */
private static final class AllFilter implements Filter {
public boolean matches(BluetoothDevice device) {
return true;
}
}
/** Filter that matches only bonded devices. */
private static final class BondedDeviceFilter implements Filter {
public boolean matches(BluetoothDevice device) {
return device.getBondState() == BluetoothDevice.BOND_BONDED;
}
}
/** Filter that matches only unbonded devices. */
private static final class UnbondedDeviceFilter implements Filter {
public boolean matches(BluetoothDevice device) {
return device.getBondState() != BluetoothDevice.BOND_BONDED;
}
}
/** Parent class of filters based on UUID and/or Bluetooth class. */
private abstract static class ClassUuidFilter implements Filter {
abstract boolean matches(ParcelUuid[] uuids, BluetoothClass btClass);
public boolean matches(BluetoothDevice device) {
return matches(device.getUuids(), device.getBluetoothClass());
}
}
/** Filter that matches devices that support AUDIO profiles. */
private static final class AudioFilter extends ClassUuidFilter {
@Override
boolean matches(ParcelUuid[] uuids, BluetoothClass btClass) {
if (uuids != null) {
if (BluetoothUuid.containsAnyUuid(uuids, A2dpProfile.SINK_UUIDS)) {
return true;
}
if (BluetoothUuid.containsAnyUuid(uuids, HeadsetProfile.UUIDS)) {
return true;
}
} else if (btClass != null) {
if (doesClassMatch(btClass, BluetoothClass.PROFILE_A2DP)
|| doesClassMatch(btClass, BluetoothClass.PROFILE_HEADSET)) {
return true;
}
}
return false;
}
}
/** Filter that matches devices that support Object Transfer. */
private static final class TransferFilter extends ClassUuidFilter {
@Override
boolean matches(ParcelUuid[] uuids, BluetoothClass btClass) {
if (uuids != null) {
if (ArrayUtils.contains(uuids, BluetoothUuid.OBEX_OBJECT_PUSH)) {
return true;
}
}
return btClass != null
&& doesClassMatch(btClass, BluetoothClass.PROFILE_OPP);
}
}
/** Filter that matches devices that support PAN User (PANU) profile. */
private static final class PanuFilter extends ClassUuidFilter {
@Override
boolean matches(ParcelUuid[] uuids, BluetoothClass btClass) {
if (uuids != null) {
if (ArrayUtils.contains(uuids, BluetoothUuid.PANU)) {
return true;
}
}
return btClass != null
&& doesClassMatch(btClass, BluetoothClass.PROFILE_PANU);
}
}
/** Filter that matches devices that support NAP profile. */
private static final class NapFilter extends ClassUuidFilter {
@Override
boolean matches(ParcelUuid[] uuids, BluetoothClass btClass) {
if (uuids != null) {
if (ArrayUtils.contains(uuids, BluetoothUuid.NAP)) {
return true;
}
}
return btClass != null
&& doesClassMatch(btClass, BluetoothClass.PROFILE_NAP);
}
}
@SuppressLint("NewApi") // Hidden API made public
private static boolean doesClassMatch(BluetoothClass btClass, int classId) {
return btClass.doesClassMatch(classId);
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2012 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.settingslib.bluetooth;
/* Required to handle timeout notification when phone is suspended */
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class BluetoothDiscoverableTimeoutReceiver extends BroadcastReceiver {
private static final String TAG = "BluetoothDiscoverableTimeoutReceiver";
private static final String INTENT_DISCOVERABLE_TIMEOUT =
"android.bluetooth.intent.DISCOVERABLE_TIMEOUT";
public static void setDiscoverableAlarm(Context context, long alarmTime) {
Log.d(TAG, "setDiscoverableAlarm(): alarmTime = " + alarmTime);
Intent intent = new Intent(INTENT_DISCOVERABLE_TIMEOUT);
intent.setClass(context, BluetoothDiscoverableTimeoutReceiver.class);
PendingIntent pending = PendingIntent.getBroadcast(
context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
AlarmManager alarmManager =
(AlarmManager) context.getSystemService (Context.ALARM_SERVICE);
if (pending != null) {
// Cancel any previous alarms that do the same thing.
alarmManager.cancel(pending);
Log.d(TAG, "setDiscoverableAlarm(): cancel prev alarm");
}
pending = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, pending);
}
public static void cancelDiscoverableAlarm(Context context) {
Log.d(TAG, "cancelDiscoverableAlarm(): Enter");
Intent intent = new Intent(INTENT_DISCOVERABLE_TIMEOUT);
intent.setClass(context, BluetoothDiscoverableTimeoutReceiver.class);
PendingIntent pending = PendingIntent.getBroadcast(
context, 0, intent, PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_IMMUTABLE);
if (pending != null) {
// Cancel any previous alarms that do the same thing.
AlarmManager alarmManager =
(AlarmManager) context.getSystemService (Context.ALARM_SERVICE);
alarmManager.cancel(pending);
}
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() == null || !intent.getAction().equals(INTENT_DISCOVERABLE_TIMEOUT)) {
return;
}
LocalBluetoothAdapter localBluetoothAdapter = LocalBluetoothAdapter.getInstance();
if(localBluetoothAdapter != null &&
localBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
Log.d(TAG, "Disable discoverable...");
localBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
} else {
Log.e(TAG, "localBluetoothAdapter is NULL!!");
}
}
}

View File

@@ -0,0 +1,555 @@
/*
* 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.settingslib.bluetooth;
import static com.android.settingslib.flags.Flags.enableCachedBluetoothDeviceDedup;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.UserHandle;
import android.telephony.TelephonyManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.settingslib.R;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* BluetoothEventManager receives broadcasts and callbacks from the Bluetooth
* API and dispatches the event on the UI thread to the right class in the
* Settings.
*/
public class BluetoothEventManager {
private static final String TAG = "BluetoothEventManager";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final LocalBluetoothAdapter mLocalAdapter;
private final CachedBluetoothDeviceManager mDeviceManager;
private final IntentFilter mAdapterIntentFilter, mProfileIntentFilter;
private final Map<String, Handler> mHandlerMap;
private final BroadcastReceiver mBroadcastReceiver = new BluetoothBroadcastReceiver();
private final BroadcastReceiver mProfileBroadcastReceiver = new BluetoothBroadcastReceiver();
private final Collection<BluetoothCallback> mCallbacks = new CopyOnWriteArrayList<>();
private final android.os.Handler mReceiverHandler;
private final UserHandle mUserHandle;
private final Context mContext;
interface Handler {
void onReceive(Context context, Intent intent, BluetoothDevice device);
}
/**
* Creates BluetoothEventManager with the ability to pass in {@link UserHandle} that tells it to
* listen for bluetooth events for that particular userHandle.
*
* <p> If passing in userHandle that's different from the user running the process,
* {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission is required. If
* userHandle passed in is {@code null}, we register event receiver for the
* {@code context.getUser()} handle.
*/
BluetoothEventManager(LocalBluetoothAdapter adapter,
CachedBluetoothDeviceManager deviceManager, Context context,
android.os.Handler handler, @Nullable UserHandle userHandle) {
mLocalAdapter = adapter;
mDeviceManager = deviceManager;
mAdapterIntentFilter = new IntentFilter();
mProfileIntentFilter = new IntentFilter();
mHandlerMap = new HashMap<>();
mContext = context;
mUserHandle = userHandle;
mReceiverHandler = handler;
// Bluetooth on/off broadcasts
addHandler(BluetoothAdapter.ACTION_STATE_CHANGED, new AdapterStateChangedHandler());
// Generic connected/not broadcast
addHandler(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED,
new ConnectionStateChangedHandler());
// Discovery broadcasts
addHandler(BluetoothAdapter.ACTION_DISCOVERY_STARTED,
new ScanningStateChangedHandler(true));
addHandler(BluetoothAdapter.ACTION_DISCOVERY_FINISHED,
new ScanningStateChangedHandler(false));
addHandler(BluetoothDevice.ACTION_FOUND, new DeviceFoundHandler());
addHandler(BluetoothDevice.ACTION_NAME_CHANGED, new NameChangedHandler());
addHandler(BluetoothDevice.ACTION_ALIAS_CHANGED, new NameChangedHandler());
// Pairing broadcasts
addHandler(BluetoothDevice.ACTION_BOND_STATE_CHANGED, new BondStateChangedHandler());
// Fine-grained state broadcasts
addHandler(BluetoothDevice.ACTION_CLASS_CHANGED, new ClassChangedHandler());
addHandler(BluetoothDevice.ACTION_UUID, new UuidChangedHandler());
addHandler(BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED, new BatteryLevelChangedHandler());
// Active device broadcasts
addHandler(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler());
addHandler(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler());
addHandler(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED,
new ActiveDeviceChangedHandler());
addHandler(BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED,
new ActiveDeviceChangedHandler());
// Headset state changed broadcasts
addHandler(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED,
new AudioModeChangedHandler());
addHandler(TelephonyManager.ACTION_PHONE_STATE_CHANGED,
new AudioModeChangedHandler());
// ACL connection changed broadcasts
addHandler(BluetoothDevice.ACTION_ACL_CONNECTED, new AclStateChangedHandler());
addHandler(BluetoothDevice.ACTION_ACL_DISCONNECTED, new AclStateChangedHandler());
registerAdapterIntentReceiver();
}
/** Register to start receiving callbacks for Bluetooth events. */
public void registerCallback(BluetoothCallback callback) {
mCallbacks.add(callback);
}
/** Unregister to stop receiving callbacks for Bluetooth events. */
public void unregisterCallback(BluetoothCallback callback) {
mCallbacks.remove(callback);
}
@VisibleForTesting
void registerProfileIntentReceiver() {
registerIntentReceiver(mProfileBroadcastReceiver, mProfileIntentFilter);
}
@VisibleForTesting
void registerAdapterIntentReceiver() {
registerIntentReceiver(mBroadcastReceiver, mAdapterIntentFilter);
}
/**
* Registers the provided receiver to receive the broadcasts that correspond to the
* passed intent filter, in the context of the provided handler.
*/
private void registerIntentReceiver(BroadcastReceiver receiver, IntentFilter filter) {
if (mUserHandle == null) {
// If userHandle has not been provided, simply call registerReceiver.
mContext.registerReceiver(receiver, filter, null, mReceiverHandler,
Context.RECEIVER_EXPORTED);
} else {
// userHandle was explicitly specified, so need to call multi-user aware API.
mContext.registerReceiverAsUser(receiver, mUserHandle, filter, null, mReceiverHandler,
Context.RECEIVER_EXPORTED);
}
}
@VisibleForTesting
void addProfileHandler(String action, Handler handler) {
mHandlerMap.put(action, handler);
mProfileIntentFilter.addAction(action);
}
boolean readPairedDevices() {
Set<BluetoothDevice> bondedDevices = mLocalAdapter.getBondedDevices();
if (bondedDevices == null) {
return false;
}
boolean deviceAdded = false;
for (BluetoothDevice device : bondedDevices) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice == null) {
mDeviceManager.addDevice(device);
deviceAdded = true;
}
}
return deviceAdded;
}
void dispatchDeviceAdded(@NonNull CachedBluetoothDevice cachedDevice) {
for (BluetoothCallback callback : mCallbacks) {
callback.onDeviceAdded(cachedDevice);
}
}
void dispatchDeviceRemoved(@NonNull CachedBluetoothDevice cachedDevice) {
for (BluetoothCallback callback : mCallbacks) {
callback.onDeviceDeleted(cachedDevice);
}
}
void dispatchProfileConnectionStateChanged(@NonNull CachedBluetoothDevice device, int state,
int bluetoothProfile) {
for (BluetoothCallback callback : mCallbacks) {
callback.onProfileConnectionStateChanged(device, state, bluetoothProfile);
}
}
private void dispatchConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {
for (BluetoothCallback callback : mCallbacks) {
callback.onConnectionStateChanged(cachedDevice, state);
}
}
private void dispatchAudioModeChanged() {
for (CachedBluetoothDevice cachedDevice : mDeviceManager.getCachedDevicesCopy()) {
cachedDevice.onAudioModeChanged();
}
for (BluetoothCallback callback : mCallbacks) {
callback.onAudioModeChanged();
}
}
@VisibleForTesting
void dispatchActiveDeviceChanged(
@Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
CachedBluetoothDevice targetDevice = activeDevice;
for (CachedBluetoothDevice cachedDevice : mDeviceManager.getCachedDevicesCopy()) {
// should report isActive from main device or it will cause trouble to other callers.
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
CachedBluetoothDevice finalTargetDevice = targetDevice;
if (targetDevice != null
&& ((subDevice != null && subDevice.equals(targetDevice))
|| cachedDevice.getMemberDevice().stream().anyMatch(
memberDevice -> memberDevice.equals(finalTargetDevice)))) {
Log.d(TAG,
"The active device is the sub/member device "
+ targetDevice.getDevice().getAnonymizedAddress()
+ ". change targetDevice as main device "
+ cachedDevice.getDevice().getAnonymizedAddress());
targetDevice = cachedDevice;
}
boolean isActiveDevice = cachedDevice.equals(targetDevice);
cachedDevice.onActiveDeviceChanged(isActiveDevice, bluetoothProfile);
mDeviceManager.onActiveDeviceChanged(cachedDevice);
}
for (BluetoothCallback callback : mCallbacks) {
callback.onActiveDeviceChanged(targetDevice, bluetoothProfile);
}
}
private void dispatchAclStateChanged(@NonNull CachedBluetoothDevice activeDevice, int state) {
for (BluetoothCallback callback : mCallbacks) {
callback.onAclConnectionStateChanged(activeDevice, state);
}
}
@VisibleForTesting
void addHandler(String action, Handler handler) {
mHandlerMap.put(action, handler);
mAdapterIntentFilter.addAction(action);
}
private class BluetoothBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
BluetoothDevice device = intent
.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Handler handler = mHandlerMap.get(action);
if (handler != null) {
handler.onReceive(context, intent, device);
}
}
}
private class AdapterStateChangedHandler implements Handler {
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR);
// update local profiles and get paired devices
mLocalAdapter.setBluetoothStateInt(state);
// send callback to update UI and possibly start scanning
for (BluetoothCallback callback : mCallbacks) {
callback.onBluetoothStateChanged(state);
}
// Inform CachedDeviceManager that the adapter state has changed
mDeviceManager.onBluetoothStateChanged(state);
}
}
private class ScanningStateChangedHandler implements Handler {
private final boolean mStarted;
ScanningStateChangedHandler(boolean started) {
mStarted = started;
}
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
for (BluetoothCallback callback : mCallbacks) {
callback.onScanningStateChanged(mStarted);
}
mDeviceManager.onScanningStateChanged(mStarted);
}
}
private class DeviceFoundHandler implements Handler {
public void onReceive(Context context, Intent intent,
BluetoothDevice device) {
short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
String name = intent.getStringExtra(BluetoothDevice.EXTRA_NAME);
final boolean isCoordinatedSetMember =
intent.getBooleanExtra(BluetoothDevice.EXTRA_IS_COORDINATED_SET_MEMBER, false);
// TODO Pick up UUID. They should be available for 2.1 devices.
// Skip for now, there's a bluez problem and we are not getting uuids even for 2.1.
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice == null) {
cachedDevice = mDeviceManager.addDevice(device);
Log.d(TAG, "DeviceFoundHandler created new CachedBluetoothDevice "
+ cachedDevice.getDevice().getAnonymizedAddress());
} else if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED
&& !cachedDevice.getDevice().isConnected()) {
// Dispatch device add callback to show bonded but
// not connected devices in discovery mode
dispatchDeviceAdded(cachedDevice);
}
cachedDevice.setRssi(rssi);
cachedDevice.setJustDiscovered(true);
cachedDevice.setIsCoordinatedSetMember(isCoordinatedSetMember);
}
}
private class ConnectionStateChangedHandler implements Handler {
@Override
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE,
BluetoothAdapter.ERROR);
dispatchConnectionStateChanged(cachedDevice, state);
}
}
private class NameChangedHandler implements Handler {
public void onReceive(Context context, Intent intent,
BluetoothDevice device) {
mDeviceManager.onDeviceNameUpdated(device);
}
}
private class BondStateChangedHandler implements Handler {
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
if (device == null) {
Log.e(TAG, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE");
return;
}
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
BluetoothDevice.ERROR);
if (mDeviceManager.onBondStateChangedIfProcess(device, bondState)) {
Log.d(TAG, "Should not update UI for the set member");
return;
}
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice == null) {
Log.w(TAG, "Got bonding state changed for " + device +
", but we have no record of that device.");
cachedDevice = mDeviceManager.addDevice(device);
}
if (enableCachedBluetoothDeviceDedup() && bondState == BluetoothDevice.BOND_BONDED) {
mDeviceManager.removeDuplicateInstanceForIdentityAddress(device);
}
for (BluetoothCallback callback : mCallbacks) {
callback.onDeviceBondStateChanged(cachedDevice, bondState);
}
cachedDevice.onBondingStateChanged(bondState);
if (bondState == BluetoothDevice.BOND_NONE) {
// Check if we need to remove other Coordinated set member devices / Hearing Aid
// devices
if (DEBUG) {
Log.d(TAG, "BondStateChangedHandler: cachedDevice.getGroupId() = "
+ cachedDevice.getGroupId() + ", cachedDevice.getHiSyncId()= "
+ cachedDevice.getHiSyncId());
}
if (cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
|| cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
Log.d(TAG, "BondStateChangedHandler: Start onDeviceUnpaired");
mDeviceManager.onDeviceUnpaired(cachedDevice);
}
int reason = intent.getIntExtra(BluetoothDevice.EXTRA_UNBOND_REASON,
BluetoothDevice.ERROR);
showUnbondMessage(context, cachedDevice.getName(), reason);
}
}
/**
* Called when we have reached the unbonded state.
*
* @param reason one of the error reasons from
* BluetoothDevice.UNBOND_REASON_*
*/
private void showUnbondMessage(Context context, String name, int reason) {
if (DEBUG) {
Log.d(TAG, "showUnbondMessage() name : " + name + ", reason : " + reason);
}
int errorMsg;
switch (reason) {
case BluetoothDevice.UNBOND_REASON_AUTH_FAILED:
errorMsg = R.string.bluetooth_pairing_pin_error_message;
break;
case BluetoothDevice.UNBOND_REASON_AUTH_REJECTED:
errorMsg = R.string.bluetooth_pairing_rejected_error_message;
break;
case BluetoothDevice.UNBOND_REASON_REMOTE_DEVICE_DOWN:
errorMsg = R.string.bluetooth_pairing_device_down_error_message;
break;
case BluetoothDevice.UNBOND_REASON_DISCOVERY_IN_PROGRESS:
case BluetoothDevice.UNBOND_REASON_AUTH_TIMEOUT:
case BluetoothDevice.UNBOND_REASON_REPEATED_ATTEMPTS:
case BluetoothDevice.UNBOND_REASON_REMOTE_AUTH_CANCELED:
errorMsg = R.string.bluetooth_pairing_error_message;
break;
default:
Log.w(TAG,
"showUnbondMessage: Not displaying any message for reason: " + reason);
return;
}
BluetoothUtils.showError(context, name, errorMsg);
}
}
private class ClassChangedHandler implements Handler {
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice != null) {
cachedDevice.refresh();
}
}
}
private class UuidChangedHandler implements Handler {
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice != null) {
cachedDevice.onUuidChanged();
}
}
}
private class BatteryLevelChangedHandler implements Handler {
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice != null) {
cachedDevice.refresh();
}
}
}
private class ActiveDeviceChangedHandler implements Handler {
@Override
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
String action = intent.getAction();
if (action == null) {
Log.w(TAG, "ActiveDeviceChangedHandler: action is null");
return;
}
@Nullable
CachedBluetoothDevice activeDevice = mDeviceManager.findDevice(device);
int bluetoothProfile = 0;
if (Objects.equals(action, BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED)) {
bluetoothProfile = BluetoothProfile.A2DP;
} else if (Objects.equals(action, BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) {
bluetoothProfile = BluetoothProfile.HEADSET;
} else if (Objects.equals(action, BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED)) {
bluetoothProfile = BluetoothProfile.HEARING_AID;
} else if (Objects.equals(action,
BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED)) {
bluetoothProfile = BluetoothProfile.LE_AUDIO;
} else {
Log.w(TAG, "ActiveDeviceChangedHandler: unknown action " + action);
return;
}
dispatchActiveDeviceChanged(activeDevice, bluetoothProfile);
}
}
private class AclStateChangedHandler implements Handler {
@Override
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
if (device == null) {
Log.w(TAG, "AclStateChangedHandler: device is null");
return;
}
// Avoid to notify Settings UI for Hearing Aid sub device.
if (mDeviceManager.isSubDevice(device)) {
return;
}
final String action = intent.getAction();
if (action == null) {
Log.w(TAG, "AclStateChangedHandler: action is null");
return;
}
final CachedBluetoothDevice activeDevice = mDeviceManager.findDevice(device);
if (activeDevice == null) {
Log.w(TAG, "AclStateChangedHandler: activeDevice is null");
return;
}
final int state;
switch (action) {
case BluetoothDevice.ACTION_ACL_CONNECTED:
state = BluetoothAdapter.STATE_CONNECTED;
break;
case BluetoothDevice.ACTION_ACL_DISCONNECTED:
state = BluetoothAdapter.STATE_DISCONNECTED;
break;
default:
Log.w(TAG, "ActiveDeviceChangedHandler: unknown action " + action);
return;
}
dispatchAclStateChanged(activeDevice, state);
}
}
private class AudioModeChangedHandler implements Handler {
@Override
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
final String action = intent.getAction();
if (action == null) {
Log.w(TAG, "AudioModeChangedHandler() action is null");
return;
}
dispatchAudioModeChanged();
}
}
}

View File

@@ -0,0 +1,387 @@
/*
* Copyright (C) 2023 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.settingslib.bluetooth
import android.annotation.TargetApi
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothLeAudioCodecConfigMetadata
import android.bluetooth.BluetoothLeAudioContentMetadata
import android.bluetooth.BluetoothLeBroadcastChannel
import android.bluetooth.BluetoothLeBroadcastMetadata
import android.bluetooth.BluetoothLeBroadcastSubgroup
import android.os.Build
import android.util.Base64
import android.util.Log
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA
object BluetoothLeBroadcastMetadataExt {
private const val TAG = "BtLeBroadcastMetadataExt"
// Data Elements for directing Broadcast Assistants
private const val KEY_BT_BROADCAST_NAME = "BN"
private const val KEY_BT_ADVERTISER_ADDRESS_TYPE = "AT"
private const val KEY_BT_ADVERTISER_ADDRESS = "AD"
private const val KEY_BT_BROADCAST_ID = "BI"
private const val KEY_BT_BROADCAST_CODE = "BC"
private const val KEY_BT_STREAM_METADATA = "MD"
private const val KEY_BT_STANDARD_QUALITY = "SQ"
private const val KEY_BT_HIGH_QUALITY = "HQ"
// Extended Bluetooth URI Data Elements
private const val KEY_BT_ADVERTISING_SID = "AS"
private const val KEY_BT_PA_INTERVAL = "PI"
private const val KEY_BT_NUM_SUBGROUPS = "NS"
// Subgroup data elements
private const val KEY_BTSG_BIS_SYNC = "BS"
private const val KEY_BTSG_NUM_BISES = "NB"
private const val KEY_BTSG_METADATA = "SM"
// Vendor specific data, not being used
private const val KEY_BTVSD_VENDOR_DATA = "VS"
private const val DELIMITER_KEY_VALUE = ":"
private const val DELIMITER_ELEMENT = ";"
private const val SUFFIX_QR_CODE = ";;"
// BT constants
private const val BIS_SYNC_MAX_CHANNEL = 32
private const val BIS_SYNC_NO_PREFERENCE = 0xFFFFFFFFu
private const val SUBGROUP_LC3_CODEC_ID = 0x6L
/**
* Converts [BluetoothLeBroadcastMetadata] to QR code string.
*
* QR code string will prefix with "BLUETOOTH:UUID:184F".
*/
fun BluetoothLeBroadcastMetadata.toQrCodeString(): String {
val entries = mutableListOf<Pair<String, String>>()
// Generate data elements for directing Broadcast Assistants
require(this.broadcastName != null) { "Broadcast name is mandatory for QR code" }
entries.add(Pair(KEY_BT_BROADCAST_NAME, Base64.encodeToString(
this.broadcastName?.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)))
entries.add(Pair(KEY_BT_ADVERTISER_ADDRESS_TYPE, this.sourceAddressType.toString()))
entries.add(Pair(KEY_BT_ADVERTISER_ADDRESS, this.sourceDevice.address.replace(":", "")))
entries.add(Pair(KEY_BT_BROADCAST_ID, String.format("%X", this.broadcastId.toLong())))
if (this.broadcastCode != null) {
entries.add(Pair(KEY_BT_BROADCAST_CODE,
Base64.encodeToString(this.broadcastCode, Base64.NO_WRAP)))
}
if (this.publicBroadcastMetadata != null &&
this.publicBroadcastMetadata?.rawMetadata?.size != 0) {
entries.add(Pair(KEY_BT_STREAM_METADATA, Base64.encodeToString(
this.publicBroadcastMetadata?.rawMetadata, Base64.NO_WRAP)))
}
if ((this.audioConfigQuality and
BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_STANDARD) != 0) {
entries.add(Pair(KEY_BT_STANDARD_QUALITY, "1"))
}
if ((this.audioConfigQuality and
BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_HIGH) != 0) {
entries.add(Pair(KEY_BT_HIGH_QUALITY, "1"))
}
// Generate extended Bluetooth URI data elements
entries.add(Pair(KEY_BT_ADVERTISING_SID,
String.format("%X", this.sourceAdvertisingSid.toLong())))
entries.add(Pair(KEY_BT_PA_INTERVAL, String.format("%X", this.paSyncInterval.toLong())))
entries.add(Pair(KEY_BT_NUM_SUBGROUPS, String.format("%X", this.subgroups.size.toLong())))
this.subgroups.forEach {
val (bisSync, bisCount) = getBisSyncFromChannels(it.channels)
entries.add(Pair(KEY_BTSG_BIS_SYNC, String.format("%X", bisSync.toLong())))
if (bisCount > 0u) {
entries.add(Pair(KEY_BTSG_NUM_BISES, String.format("%X", bisCount.toLong())))
}
if (it.contentMetadata.rawMetadata.size != 0) {
entries.add(Pair(KEY_BTSG_METADATA,
Base64.encodeToString(it.contentMetadata.rawMetadata, Base64.NO_WRAP)))
}
}
val qrCodeString = SCHEME_BT_BROADCAST_METADATA +
entries.toQrCodeString(DELIMITER_ELEMENT) + SUFFIX_QR_CODE
Log.d(TAG, "Generated QR string : $qrCodeString")
return qrCodeString
}
/**
* Converts QR code string to [BluetoothLeBroadcastMetadata].
*
* QR code string should prefix with "BLUETOOTH:UUID:184F".
*/
fun convertToBroadcastMetadata(qrCodeString: String): BluetoothLeBroadcastMetadata? {
if (!qrCodeString.startsWith(SCHEME_BT_BROADCAST_METADATA)) {
Log.e(TAG, "String \"$qrCodeString\" does not begin with " +
"\"$SCHEME_BT_BROADCAST_METADATA\"")
return null
}
return try {
Log.d(TAG, "Parsing QR string: $qrCodeString")
val strippedString =
qrCodeString.removePrefix(SCHEME_BT_BROADCAST_METADATA)
.removeSuffix(SUFFIX_QR_CODE)
Log.d(TAG, "Stripped to: $strippedString")
parseQrCodeToMetadata(strippedString)
} catch (e: Exception) {
Log.w(TAG, "Cannot parse: $qrCodeString", e)
null
}
}
private fun List<Pair<String, String>>.toQrCodeString(delimiter: String): String {
val entryStrings = this.map{ it.first + DELIMITER_KEY_VALUE + it.second }
return entryStrings.joinToString(separator = delimiter)
}
@TargetApi(Build.VERSION_CODES.TIRAMISU)
private fun parseQrCodeToMetadata(input: String): BluetoothLeBroadcastMetadata {
// Split into a list of list
val elementFields = input.split(DELIMITER_ELEMENT)
.map{it.split(DELIMITER_KEY_VALUE, limit = 2)}
var sourceAddrType = BluetoothDevice.ADDRESS_TYPE_UNKNOWN
var sourceAddrString: String? = null
var sourceAdvertiserSid = -1
var broadcastId = -1
var broadcastName: String? = null
var streamMetadata: BluetoothLeAudioContentMetadata? = null
var paSyncInterval = -1
var broadcastCode: ByteArray? = null
var audioConfigQualityStandard = -1
var audioConfigQualityHigh = -1
var numSubgroups = -1
// List of subgroup data
var subgroupBisSyncList = mutableListOf<UInt>()
var subgroupNumOfBisesList = mutableListOf<UInt>()
var subgroupMetadataList = mutableListOf<ByteArray?>()
val builder = BluetoothLeBroadcastMetadata.Builder()
for (field: List<String> in elementFields) {
if (field.isEmpty()) {
continue
}
val key = field[0]
// Ignore 3rd value and after
val value = if (field.size > 1) field[1] else ""
when (key) {
// Parse data elements for directing Broadcast Assistants
KEY_BT_BROADCAST_NAME -> {
require(broadcastName == null) { "Duplicate broadcastName: $input" }
broadcastName = String(Base64.decode(value, Base64.NO_WRAP))
}
KEY_BT_ADVERTISER_ADDRESS_TYPE -> {
require(sourceAddrType == BluetoothDevice.ADDRESS_TYPE_UNKNOWN) {
"Duplicate sourceAddrType: $input"
}
sourceAddrType = value.toInt()
}
KEY_BT_ADVERTISER_ADDRESS -> {
require(sourceAddrString == null) { "Duplicate sourceAddr: $input" }
sourceAddrString = value.chunked(2).joinToString(":")
}
KEY_BT_BROADCAST_ID -> {
require(broadcastId == -1) { "Duplicate broadcastId: $input" }
broadcastId = value.toInt(16)
}
KEY_BT_BROADCAST_CODE -> {
require(broadcastCode == null) { "Duplicate broadcastCode: $input" }
broadcastCode = Base64.decode(value.dropLastWhile { it.equals(0.toByte()) }
.toByteArray(), Base64.NO_WRAP)
}
KEY_BT_STREAM_METADATA -> {
require(streamMetadata == null) {
"Duplicate streamMetadata $input"
}
streamMetadata = BluetoothLeAudioContentMetadata
.fromRawBytes(Base64.decode(value, Base64.NO_WRAP))
}
KEY_BT_STANDARD_QUALITY -> {
require(audioConfigQualityStandard == -1) {
"Duplicate audioConfigQualityStandard: $input"
}
audioConfigQualityStandard = value.toInt()
}
KEY_BT_HIGH_QUALITY -> {
require(audioConfigQualityHigh == -1) {
"Duplicate audioConfigQualityHigh: $input"
}
audioConfigQualityHigh = value.toInt()
}
// Parse extended Bluetooth URI data elements
KEY_BT_ADVERTISING_SID -> {
require(sourceAdvertiserSid == -1) { "Duplicate sourceAdvertiserSid: $input" }
sourceAdvertiserSid = value.toInt(16)
}
KEY_BT_PA_INTERVAL -> {
require(paSyncInterval == -1) { "Duplicate paSyncInterval: $input" }
paSyncInterval = value.toInt(16)
}
KEY_BT_NUM_SUBGROUPS -> {
require(numSubgroups == -1) { "Duplicate numSubgroups: $input" }
numSubgroups = value.toInt(16)
}
// Repeatable subgroup elements
KEY_BTSG_BIS_SYNC -> {
subgroupBisSyncList.add(value.toUInt(16))
}
KEY_BTSG_NUM_BISES -> {
subgroupNumOfBisesList.add(value.toUInt(16))
}
KEY_BTSG_METADATA -> {
subgroupMetadataList.add(Base64.decode(value, Base64.NO_WRAP))
}
}
}
Log.d(TAG, "parseQrCodeToMetadata: main data elements sourceAddrType=$sourceAddrType, " +
"sourceAddr=$sourceAddrString, sourceAdvertiserSid=$sourceAdvertiserSid, " +
"broadcastId=$broadcastId, broadcastName=$broadcastName, " +
"streamMetadata=${streamMetadata != null}, " +
"paSyncInterval=$paSyncInterval, " +
"broadcastCode=${broadcastCode?.toString(Charsets.UTF_8)}, " +
"audioConfigQualityStandard=$audioConfigQualityStandard, " +
"audioConfigQualityHigh=$audioConfigQualityHigh")
val adapter = BluetoothAdapter.getDefaultAdapter()
// Check parsed elements data
require(broadcastName != null) {
"broadcastName($broadcastName) must present in QR code string"
}
var addr = sourceAddrString
var addrType = sourceAddrType
if (sourceAddrString != null) {
require(sourceAddrType != BluetoothDevice.ADDRESS_TYPE_UNKNOWN) {
"sourceAddrType($sourceAddrType) must present if address present"
}
} else {
// Use placeholder device if not present
addr = "FF:FF:FF:FF:FF:FF"
addrType = BluetoothDevice.ADDRESS_TYPE_RANDOM
}
val device = adapter.getRemoteLeDevice(requireNotNull(addr), addrType)
// add source device and set broadcast code
var audioConfigQuality = BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_NONE or
(if (audioConfigQualityStandard != -1) audioConfigQualityStandard else 0) or
(if (audioConfigQualityHigh != -1) audioConfigQualityHigh else 0)
// process subgroup data
// metadata should include at least 1 subgroup for metadata, add a placeholder group if not present
numSubgroups = if (numSubgroups > 0) numSubgroups else 1
for (i in 0 until numSubgroups) {
val bisSync = subgroupBisSyncList.getOrNull(i)
val bisNum = subgroupNumOfBisesList.getOrNull(i)
val metadata = subgroupMetadataList.getOrNull(i)
val channels = convertToChannels(bisSync, bisNum)
val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder()
.setAudioLocation(0).build()
val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
setCodecId(SUBGROUP_LC3_CODEC_ID)
setCodecSpecificConfig(audioCodecConfigMetadata)
setContentMetadata(
BluetoothLeAudioContentMetadata.fromRawBytes(metadata ?: ByteArray(0)))
channels.forEach(::addChannel)
}.build()
Log.d(TAG, "parseQrCodeToMetadata: subgroup $i elements bisSync=$bisSync, " +
"bisNum=$bisNum, metadata=${metadata != null}")
builder.addSubgroup(subgroup)
}
builder.apply {
setSourceDevice(device, sourceAddrType)
setSourceAdvertisingSid(sourceAdvertiserSid)
setBroadcastId(broadcastId)
setBroadcastName(broadcastName)
// QR code should set PBP(public broadcast profile) for auracast
setPublicBroadcast(true)
setPublicBroadcastMetadata(streamMetadata)
setPaSyncInterval(paSyncInterval)
setEncrypted(broadcastCode != null)
setBroadcastCode(broadcastCode)
// Presentation delay is unknown and not useful when adding source
// Broadcast sink needs to sync to the Broadcast source to get presentation delay
setPresentationDelayMicros(0)
setAudioConfigQuality(audioConfigQuality)
}
return builder.build()
}
private fun getBisSyncFromChannels(
channels: List<BluetoothLeBroadcastChannel>
): Pair<UInt, UInt> {
var bisSync = 0u
var bisCount = 0u
// channel index starts from 1
channels.forEach { channel ->
if (channel.channelIndex > 0) {
bisCount++
if (channel.isSelected) {
bisSync = bisSync or (1u shl (channel.channelIndex - 1))
}
}
}
// No channel is selected means no preference on Android platform
return if (bisSync == 0u) Pair(BIS_SYNC_NO_PREFERENCE, bisCount)
else Pair(bisSync, bisCount)
}
private fun convertToChannels(
bisSync: UInt?,
bisNum: UInt?
): List<BluetoothLeBroadcastChannel> {
Log.d(TAG, "convertToChannels: bisSync=$bisSync, bisNum=$bisNum")
// if no BIS_SYNC or BIS_NUM available or BIS_SYNC is no preference
// return empty channel map with one placeholder channel
var selectedChannels = if (bisSync != null && bisNum != null) bisSync else 0u
val channels = mutableListOf<BluetoothLeBroadcastChannel>()
val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder()
.setAudioLocation(0).build()
if (bisSync == BIS_SYNC_NO_PREFERENCE || selectedChannels == 0u) {
// No channel preference means no channel is selected
// Generate one placeholder channel for metadata
val channel = BluetoothLeBroadcastChannel.Builder().apply {
setSelected(false)
setChannelIndex(1)
setCodecMetadata(audioCodecConfigMetadata)
}
return listOf(channel.build())
}
for (i in 0 until BIS_SYNC_MAX_CHANNEL) {
val channelMask = 1u shl i
if ((selectedChannels and channelMask) != 0u) {
val channel = BluetoothLeBroadcastChannel.Builder().apply {
setSelected(true)
setChannelIndex(i + 1)
setCodecMetadata(audioCodecConfigMetadata)
}
channels.add(channel.build())
}
}
return channels
}
}

View File

@@ -0,0 +1,719 @@
package com.android.settingslib.bluetooth;
import static com.android.settingslib.widget.AdaptiveOutlineDrawable.ICON_TYPE_ADVANCED;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.net.Uri;
import android.provider.DeviceConfig;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.core.graphics.drawable.IconCompat;
import com.android.settingslib.R;
import com.android.settingslib.widget.AdaptiveIcon;
import com.android.settingslib.widget.AdaptiveOutlineDrawable;
import com.google.common.collect.ImmutableSet;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BluetoothUtils {
private static final String TAG = "BluetoothUtils";
public static final boolean V = false; // verbose logging
public static final boolean D = true; // regular logging
public static final int META_INT_ERROR = -1;
public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled";
private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH";
private static final Set<String> EXCLUSIVE_MANAGERS = ImmutableSet.of(
"com.google.android.gms.dck");
private static ErrorListener sErrorListener;
public static int getConnectionStateSummary(int connectionState) {
switch (connectionState) {
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_connected;
case BluetoothProfile.STATE_CONNECTING:
return R.string.bluetooth_connecting;
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_disconnected;
case BluetoothProfile.STATE_DISCONNECTING:
return R.string.bluetooth_disconnecting;
default:
return 0;
}
}
static void showError(Context context, String name, int messageResId) {
if (sErrorListener != null) {
sErrorListener.onShowError(context, name, messageResId);
}
}
public static void setErrorListener(ErrorListener listener) {
sErrorListener = listener;
}
public interface ErrorListener {
void onShowError(Context context, String name, int messageResId);
}
/**
* @param context to access resources from
* @param cachedDevice to get class from
* @return pair containing the drawable and the description of the Bluetooth class
* of the device.
*/
public static Pair<Drawable, String> getBtClassDrawableWithDescription(Context context,
CachedBluetoothDevice cachedDevice) {
BluetoothClass btClass = cachedDevice.getBtClass();
if (btClass != null) {
switch (btClass.getMajorDeviceClass()) {
case BluetoothClass.Device.Major.COMPUTER:
return new Pair<>(getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_bt_laptop),
context.getString(R.string.bluetooth_talkback_computer));
case BluetoothClass.Device.Major.PHONE:
return new Pair<>(
getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_phone),
context.getString(R.string.bluetooth_talkback_phone));
case BluetoothClass.Device.Major.PERIPHERAL:
return new Pair<>(
getBluetoothDrawable(context, HidProfile.getHidClassDrawable(btClass)),
context.getString(R.string.bluetooth_talkback_input_peripheral));
case BluetoothClass.Device.Major.IMAGING:
return new Pair<>(
getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_settings_print),
context.getString(R.string.bluetooth_talkback_imaging));
default:
// unrecognized device class; continue
}
}
if (cachedDevice.isHearingAidDevice()) {
return new Pair<>(getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_bt_hearing_aid),
context.getString(R.string.bluetooth_talkback_hearing_aids));
}
List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles();
int resId = 0;
for (LocalBluetoothProfile profile : profiles) {
int profileResId = profile.getDrawableResource(btClass);
if (profileResId != 0) {
// The device should show hearing aid icon if it contains any hearing aid related
// profiles
if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) {
return new Pair<>(getBluetoothDrawable(context, profileResId),
context.getString(R.string.bluetooth_talkback_hearing_aids));
}
if (resId == 0) {
resId = profileResId;
}
}
}
if (resId != 0) {
return new Pair<>(getBluetoothDrawable(context, resId), null);
}
if (btClass != null) {
if (doesClassMatch(btClass, BluetoothClass.PROFILE_HEADSET)) {
return new Pair<>(
getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_bt_headset_hfp),
context.getString(R.string.bluetooth_talkback_headset));
}
if (doesClassMatch(btClass, BluetoothClass.PROFILE_A2DP)) {
return new Pair<>(
getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_bt_headphones_a2dp),
context.getString(R.string.bluetooth_talkback_headphone));
}
}
return new Pair<>(
getBluetoothDrawable(context,
com.android.internal.R.drawable.ic_settings_bluetooth).mutate(),
context.getString(R.string.bluetooth_talkback_bluetooth));
}
/**
* Get bluetooth drawable by {@code resId}
*/
public static Drawable getBluetoothDrawable(Context context, @DrawableRes int resId) {
return context.getDrawable(resId);
}
/**
* Get colorful bluetooth icon with description
*/
public static Pair<Drawable, String> getBtRainbowDrawableWithDescription(Context context,
CachedBluetoothDevice cachedDevice) {
final Resources resources = context.getResources();
final Pair<Drawable, String> pair = BluetoothUtils.getBtDrawableWithDescription(context,
cachedDevice);
if (pair.first instanceof BitmapDrawable) {
return new Pair<>(new AdaptiveOutlineDrawable(
resources, ((BitmapDrawable) pair.first).getBitmap()), pair.second);
}
int hashCode;
if ((cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID)) {
hashCode = new Integer(cachedDevice.getGroupId()).hashCode();
} else {
hashCode = cachedDevice.getAddress().hashCode();
}
return new Pair<>(buildBtRainbowDrawable(context,
pair.first, hashCode), pair.second);
}
/**
* Build Bluetooth device icon with rainbow
*/
private static Drawable buildBtRainbowDrawable(Context context, Drawable drawable,
int hashCode) {
final Resources resources = context.getResources();
// Deal with normal headset
final int[] iconFgColors = resources.getIntArray(R.array.bt_icon_fg_colors);
final int[] iconBgColors = resources.getIntArray(R.array.bt_icon_bg_colors);
// get color index based on mac address
final int index = Math.abs(hashCode % iconBgColors.length);
drawable.setTint(iconFgColors[index]);
final Drawable adaptiveIcon = new AdaptiveIcon(context, drawable);
((AdaptiveIcon) adaptiveIcon).setBackgroundColor(iconBgColors[index]);
return adaptiveIcon;
}
/**
* Get bluetooth icon with description
*/
public static Pair<Drawable, String> getBtDrawableWithDescription(Context context,
CachedBluetoothDevice cachedDevice) {
final Pair<Drawable, String> pair = BluetoothUtils.getBtClassDrawableWithDescription(
context, cachedDevice);
final BluetoothDevice bluetoothDevice = cachedDevice.getDevice();
final int iconSize = context.getResources().getDimensionPixelSize(
R.dimen.bt_nearby_icon_size);
final Resources resources = context.getResources();
// Deal with advanced device icon
if (isAdvancedDetailsHeader(bluetoothDevice)) {
final Uri iconUri = getUriMetaData(bluetoothDevice,
BluetoothDevice.METADATA_MAIN_ICON);
if (iconUri != null) {
try {
context.getContentResolver().takePersistableUriPermission(iconUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION);
} catch (SecurityException e) {
Log.e(TAG, "Failed to take persistable permission for: " + iconUri, e);
}
try {
final Bitmap bitmap = MediaStore.Images.Media.getBitmap(
context.getContentResolver(), iconUri);
if (bitmap != null) {
final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize,
iconSize, false);
bitmap.recycle();
return new Pair<>(new BitmapDrawable(resources,
resizedBitmap), pair.second);
}
} catch (IOException e) {
Log.e(TAG, "Failed to get drawable for: " + iconUri, e);
} catch (SecurityException e) {
Log.e(TAG, "Failed to get permission for: " + iconUri, e);
}
}
}
return new Pair<>(pair.first, pair.second);
}
/**
* Check if the Bluetooth device supports advanced metadata
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @return true if it supports advanced metadata, false otherwise.
*/
public static boolean isAdvancedDetailsHeader(@NonNull BluetoothDevice bluetoothDevice) {
if (!isAdvancedHeaderEnabled()) {
return false;
}
if (isUntetheredHeadset(bluetoothDevice)) {
return true;
}
// The metadata is for Android S
String deviceType = getStringMetaData(bluetoothDevice,
BluetoothDevice.METADATA_DEVICE_TYPE);
if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)
|| TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_WATCH)
|| TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_DEFAULT)
|| TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_STYLUS)) {
Log.d(TAG, "isAdvancedDetailsHeader: deviceType is " + deviceType);
return true;
}
return false;
}
/**
* Check if the Bluetooth device is supports advanced metadata and an untethered headset
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @return true if it supports advanced metadata and an untethered headset, false otherwise.
*/
public static boolean isAdvancedUntetheredDevice(@NonNull BluetoothDevice bluetoothDevice) {
if (!isAdvancedHeaderEnabled()) {
return false;
}
if (isUntetheredHeadset(bluetoothDevice)) {
return true;
}
// The metadata is for Android S
String deviceType = getStringMetaData(bluetoothDevice,
BluetoothDevice.METADATA_DEVICE_TYPE);
if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)) {
Log.d(TAG, "isAdvancedUntetheredDevice: is untethered device ");
return true;
}
return false;
}
/**
* Check if a device class matches with a defined BluetoothClass device.
*
* @param device Must be one of the public constants in {@link BluetoothClass.Device}
* @return true if device class matches, false otherwise.
*/
public static boolean isDeviceClassMatched(@NonNull BluetoothDevice bluetoothDevice,
int device) {
final BluetoothClass bluetoothClass = bluetoothDevice.getBluetoothClass();
return bluetoothClass != null && bluetoothClass.getDeviceClass() == device;
}
private static boolean isAdvancedHeaderEnabled() {
if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED,
true)) {
Log.d(TAG, "isAdvancedDetailsHeader: advancedEnabled is false");
return false;
}
return true;
}
private static boolean isUntetheredHeadset(@NonNull BluetoothDevice bluetoothDevice) {
// The metadata is for Android R
if (getBooleanMetaData(bluetoothDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) {
Log.d(TAG, "isAdvancedDetailsHeader: untetheredHeadset is true");
return true;
}
return false;
}
/**
* Create an Icon pointing to a drawable.
*/
public static IconCompat createIconWithDrawable(Drawable drawable) {
Bitmap bitmap;
if (drawable instanceof BitmapDrawable) {
bitmap = ((BitmapDrawable) drawable).getBitmap();
} else {
final int width = drawable.getIntrinsicWidth();
final int height = drawable.getIntrinsicHeight();
bitmap = createBitmap(drawable,
width > 0 ? width : 1,
height > 0 ? height : 1);
}
return IconCompat.createWithBitmap(bitmap);
}
/**
* Build device icon with advanced outline
*/
public static Drawable buildAdvancedDrawable(Context context, Drawable drawable) {
final int iconSize = context.getResources().getDimensionPixelSize(
R.dimen.advanced_icon_size);
final Resources resources = context.getResources();
Bitmap bitmap = null;
if (drawable instanceof BitmapDrawable) {
bitmap = ((BitmapDrawable) drawable).getBitmap();
} else {
final int width = drawable.getIntrinsicWidth();
final int height = drawable.getIntrinsicHeight();
bitmap = createBitmap(drawable,
width > 0 ? width : 1,
height > 0 ? height : 1);
}
if (bitmap != null) {
final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize,
iconSize, false);
bitmap.recycle();
return new AdaptiveOutlineDrawable(resources, resizedBitmap, ICON_TYPE_ADVANCED);
}
return drawable;
}
/**
* Creates a drawable with specified width and height.
*/
public static Bitmap createBitmap(Drawable drawable, int width, int height) {
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
/**
* Get boolean Bluetooth metadata
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @param key key value within the list of BluetoothDevice.METADATA_*
* @return the boolean metdata
*/
public static boolean getBooleanMetaData(BluetoothDevice bluetoothDevice, int key) {
if (bluetoothDevice == null) {
return false;
}
final byte[] data = bluetoothDevice.getMetadata(key);
if (data == null) {
return false;
}
return Boolean.parseBoolean(new String(data));
}
/**
* Get String Bluetooth metadata
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @param key key value within the list of BluetoothDevice.METADATA_*
* @return the String metdata
*/
public static String getStringMetaData(BluetoothDevice bluetoothDevice, int key) {
if (bluetoothDevice == null) {
return null;
}
final byte[] data = bluetoothDevice.getMetadata(key);
if (data == null) {
return null;
}
return new String(data);
}
/**
* Get integer Bluetooth metadata
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @param key key value within the list of BluetoothDevice.METADATA_*
* @return the int metdata
*/
public static int getIntMetaData(BluetoothDevice bluetoothDevice, int key) {
if (bluetoothDevice == null) {
return META_INT_ERROR;
}
final byte[] data = bluetoothDevice.getMetadata(key);
if (data == null) {
return META_INT_ERROR;
}
try {
return Integer.parseInt(new String(data));
} catch (NumberFormatException e) {
return META_INT_ERROR;
}
}
/**
* Get URI Bluetooth metadata
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @param key key value within the list of BluetoothDevice.METADATA_*
* @return the URI metdata
*/
public static Uri getUriMetaData(BluetoothDevice bluetoothDevice, int key) {
String data = getStringMetaData(bluetoothDevice, key);
if (data == null) {
return null;
}
return Uri.parse(data);
}
/**
* Get URI Bluetooth metadata for extra control
*
* @param bluetoothDevice the BluetoothDevice to get metadata
* @return the URI metadata
*/
public static String getControlUriMetaData(BluetoothDevice bluetoothDevice) {
String data = getStringMetaData(bluetoothDevice, METADATA_FAST_PAIR_CUSTOMIZED_FIELDS);
return extraTagValue(KEY_HEARABLE_CONTROL_SLICE, data);
}
/**
* Check if the Bluetooth device is an AvailableMediaBluetoothDevice, which means:
* 1) currently connected
* 2) is Hearing Aid or LE Audio
* OR
* 3) connected profile matches currentAudioProfile
*
* @param cachedDevice the CachedBluetoothDevice
* @param audioManager audio manager to get the current audio profile
* @return if the device is AvailableMediaBluetoothDevice
*/
@WorkerThread
public static boolean isAvailableMediaBluetoothDevice(
CachedBluetoothDevice cachedDevice, AudioManager audioManager) {
int audioMode = audioManager.getMode();
int currentAudioProfile;
if (audioMode == AudioManager.MODE_RINGTONE
|| audioMode == AudioManager.MODE_IN_CALL
|| audioMode == AudioManager.MODE_IN_COMMUNICATION) {
// in phone call
currentAudioProfile = BluetoothProfile.HEADSET;
} else {
// without phone call
currentAudioProfile = BluetoothProfile.A2DP;
}
boolean isFilterMatched = false;
if (isDeviceConnected(cachedDevice)) {
// If device is Hearing Aid or LE Audio, it is compatible with HFP and A2DP.
// It would show in Available Devices group.
if (cachedDevice.isConnectedAshaHearingAidDevice()
|| cachedDevice.isConnectedLeAudioDevice()) {
Log.d(TAG, "isFilterMatched() device : "
+ cachedDevice.getName() + ", the profile is connected.");
return true;
}
// According to the current audio profile type,
// this page will show the bluetooth device that have corresponding profile.
// For example:
// If current audio profile is a2dp, show the bluetooth device that have a2dp profile.
// If current audio profile is headset,
// show the bluetooth device that have headset profile.
switch (currentAudioProfile) {
case BluetoothProfile.A2DP:
isFilterMatched = cachedDevice.isConnectedA2dpDevice();
break;
case BluetoothProfile.HEADSET:
isFilterMatched = cachedDevice.isConnectedHfpDevice();
break;
}
}
return isFilterMatched;
}
/**
* Check if the Bluetooth device is a ConnectedBluetoothDevice, which means:
* 1) currently connected
* 2) is not Hearing Aid or LE Audio
* AND
* 3) connected profile does not match currentAudioProfile
*
* @param cachedDevice the CachedBluetoothDevice
* @param audioManager audio manager to get the current audio profile
* @return if the device is AvailableMediaBluetoothDevice
*/
@WorkerThread
public static boolean isConnectedBluetoothDevice(
CachedBluetoothDevice cachedDevice, AudioManager audioManager) {
int audioMode = audioManager.getMode();
int currentAudioProfile;
if (audioMode == AudioManager.MODE_RINGTONE
|| audioMode == AudioManager.MODE_IN_CALL
|| audioMode == AudioManager.MODE_IN_COMMUNICATION) {
// in phone call
currentAudioProfile = BluetoothProfile.HEADSET;
} else {
// without phone call
currentAudioProfile = BluetoothProfile.A2DP;
}
boolean isFilterMatched = false;
if (isDeviceConnected(cachedDevice)) {
// If device is Hearing Aid or LE Audio, it is compatible with HFP and A2DP.
// It would not show in Connected Devices group.
if (cachedDevice.isConnectedAshaHearingAidDevice()
|| cachedDevice.isConnectedLeAudioDevice()) {
return false;
}
// According to the current audio profile type,
// this page will show the bluetooth device that doesn't have corresponding profile.
// For example:
// If current audio profile is a2dp,
// show the bluetooth device that doesn't have a2dp profile.
// If current audio profile is headset,
// show the bluetooth device that doesn't have headset profile.
switch (currentAudioProfile) {
case BluetoothProfile.A2DP:
isFilterMatched = !cachedDevice.isConnectedA2dpDevice();
break;
case BluetoothProfile.HEADSET:
isFilterMatched = !cachedDevice.isConnectedHfpDevice();
break;
}
}
return isFilterMatched;
}
/**
* Check if the Bluetooth device is an active media device
*
* @param cachedDevice the CachedBluetoothDevice
* @return if the Bluetooth device is an active media device
*/
public static boolean isActiveMediaDevice(CachedBluetoothDevice cachedDevice) {
return cachedDevice.isActiveDevice(BluetoothProfile.A2DP)
|| cachedDevice.isActiveDevice(BluetoothProfile.HEADSET)
|| cachedDevice.isActiveDevice(BluetoothProfile.HEARING_AID)
|| cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO);
}
/**
* Check if the Bluetooth device is an active LE Audio device
*
* @param cachedDevice the CachedBluetoothDevice
* @return if the Bluetooth device is an active LE Audio device
*/
public static boolean isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice) {
return cachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO);
}
private static boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) {
if (cachedDevice == null) {
return false;
}
final BluetoothDevice device = cachedDevice.getDevice();
return device.getBondState() == BluetoothDevice.BOND_BONDED && device.isConnected();
}
@SuppressLint("NewApi") // Hidden API made public
private static boolean doesClassMatch(BluetoothClass btClass, int classId) {
return btClass.doesClassMatch(classId);
}
private static String extraTagValue(String tag, String metaData) {
if (TextUtils.isEmpty(metaData)) {
return null;
}
Pattern pattern = Pattern.compile(generateExpressionWithTag(tag, "(.*?)"));
Matcher matcher = pattern.matcher(metaData);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
private static String getTagStart(String tag) {
return String.format(Locale.ENGLISH, "<%s>", tag);
}
private static String getTagEnd(String tag) {
return String.format(Locale.ENGLISH, "</%s>", tag);
}
private static String generateExpressionWithTag(String tag, String value) {
return getTagStart(tag) + value + getTagEnd(tag);
}
/**
* Returns the BluetoothDevice's exclusive manager
* ({@link BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists and is in the
* given set, otherwise null.
*/
@Nullable
private static String getAllowedExclusiveManager(BluetoothDevice bluetoothDevice) {
byte[] exclusiveManagerNameBytes = bluetoothDevice.getMetadata(
BluetoothDevice.METADATA_EXCLUSIVE_MANAGER);
if (exclusiveManagerNameBytes == null) {
Log.d(TAG, "Bluetooth device " + bluetoothDevice.getName()
+ " doesn't have exclusive manager");
return null;
}
String exclusiveManagerName = new String(exclusiveManagerNameBytes);
return getExclusiveManagers().contains(exclusiveManagerName) ? exclusiveManagerName
: null;
}
/**
* Checks if given package is installed
*/
private static boolean isPackageInstalled(Context context,
String packageName) {
PackageManager packageManager = context.getPackageManager();
try {
packageManager.getPackageInfo(packageName, 0);
return true;
} catch (PackageManager.NameNotFoundException e) {
Log.d(TAG, "Package " + packageName + " is not installed");
}
return false;
}
/**
* A BluetoothDevice is exclusively managed if
* 1) it has field {@link BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata.
* 2) the exclusive manager app name is in the allowlist.
* 3) the exclusive manager app is installed.
*/
public static boolean isExclusivelyManagedBluetoothDevice(@NonNull Context context,
@NonNull BluetoothDevice bluetoothDevice) {
String exclusiveManagerName = getAllowedExclusiveManager(bluetoothDevice);
if (exclusiveManagerName == null) {
return false;
}
if (!isPackageInstalled(context, exclusiveManagerName)) {
return false;
} else {
Log.d(TAG, "Found exclusively managed app " + exclusiveManagerName);
return true;
}
}
/**
* Return the allowlist for exclusive manager names.
*/
@NonNull
public static Set<String> getExclusiveManagers() {
return EXCLUSIVE_MANAGERS;
}
}

View File

@@ -0,0 +1,67 @@
/**
* 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.settingslib.bluetooth;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.TextView;
import com.android.settingslib.R;
public class BroadcastDialog extends AlertDialog {
private static final String TAG = "BroadcastDialog";
private String mCurrentApp;
private String mSwitchApp;
private Context mContext;
public BroadcastDialog(Context context) {
super(context);
mContext = context;
}
@Override
public void onCreate(Bundle savedInstanceState) {
View layout = View.inflate(mContext, R.layout.broadcast_dialog, null);
final Window window = getWindow();
window.setContentView(layout);
window.setWindowAnimations(
com.android.settingslib.widget.theme.R.style.Theme_AlertDialog_SettingsLib);
TextView title = layout.findViewById(R.id.dialog_title);
TextView subTitle = layout.findViewById(R.id.dialog_subtitle);
title.setText(mContext.getString(R.string.bt_le_audio_broadcast_dialog_title, mCurrentApp));
subTitle.setText(
mContext.getString(R.string.bt_le_audio_broadcast_dialog_sub_title, mSwitchApp));
Button positiveBtn = layout.findViewById(R.id.positive_btn);
Button negativeBtn = layout.findViewById(R.id.negative_btn);
Button neutralBtn = layout.findViewById(R.id.neutral_btn);
positiveBtn.setText(mContext.getString(
R.string.bt_le_audio_broadcast_dialog_switch_app, mSwitchApp), null);
neutralBtn.setOnClickListener((view) -> {
Log.d(TAG, "BroadcastDialog dismiss.");
dismiss();
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,570 @@
/*
* Copyright (C) 2008 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.settingslib.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* CachedBluetoothDeviceManager manages the set of remote Bluetooth devices.
*/
public class CachedBluetoothDeviceManager {
private static final String TAG = "CachedBluetoothDeviceManager";
private static final boolean DEBUG = BluetoothUtils.D;
@VisibleForTesting static int sLateBondingTimeoutMillis = 5000; // 5s
private Context mContext;
private final LocalBluetoothManager mBtManager;
@VisibleForTesting
final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<CachedBluetoothDevice>();
@VisibleForTesting
HearingAidDeviceManager mHearingAidDeviceManager;
@VisibleForTesting
CsipDeviceManager mCsipDeviceManager;
BluetoothDevice mOngoingSetMemberPair;
boolean mIsLateBonding;
int mGroupIdOfLateBonding;
public CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) {
mContext = context;
mBtManager = localBtManager;
mHearingAidDeviceManager = new HearingAidDeviceManager(context, localBtManager,
mCachedDevices);
mCsipDeviceManager = new CsipDeviceManager(localBtManager, mCachedDevices);
}
public synchronized Collection<CachedBluetoothDevice> getCachedDevicesCopy() {
return new ArrayList<>(mCachedDevices);
}
public static boolean onDeviceDisappeared(CachedBluetoothDevice cachedDevice) {
cachedDevice.setJustDiscovered(false);
return cachedDevice.getBondState() == BluetoothDevice.BOND_NONE;
}
public void onDeviceNameUpdated(BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = findDevice(device);
if (cachedDevice != null) {
cachedDevice.refreshName();
}
}
/**
* Search for existing {@link CachedBluetoothDevice} or return null
* if this device isn't in the cache. Use {@link #addDevice}
* to create and return a new {@link CachedBluetoothDevice} for
* a newly discovered {@link BluetoothDevice}.
*
* @param device the address of the Bluetooth device
* @return the cached device object for this device, or null if it has
* not been previously seen
*/
public synchronized CachedBluetoothDevice findDevice(BluetoothDevice device) {
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
if (cachedDevice.getDevice().equals(device)) {
return cachedDevice;
}
// Check the member devices for the coordinated set if it exists
final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
if (!memberDevices.isEmpty()) {
for (CachedBluetoothDevice memberDevice : memberDevices) {
if (memberDevice.getDevice().equals(device)) {
return memberDevice;
}
}
}
// Check sub devices for hearing aid if it exists
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null && subDevice.getDevice().equals(device)) {
return subDevice;
}
}
return null;
}
/**
* Create and return a new {@link CachedBluetoothDevice}. This assumes
* that {@link #findDevice} has already been called and returned null.
* @param device the new Bluetooth device
* @return the newly created CachedBluetoothDevice object
*/
public CachedBluetoothDevice addDevice(BluetoothDevice device) {
return addDevice(device, /*leScanFilters=*/null);
}
/**
* Create and return a new {@link CachedBluetoothDevice}. This assumes
* that {@link #findDevice} has already been called and returned null.
* @param device the new Bluetooth device
* @param leScanFilters the BLE scan filters which the device matched
* @return the newly created CachedBluetoothDevice object
*/
public CachedBluetoothDevice addDevice(BluetoothDevice device, List<ScanFilter> leScanFilters) {
CachedBluetoothDevice newDevice;
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
synchronized (this) {
newDevice = findDevice(device);
if (newDevice == null) {
newDevice = new CachedBluetoothDevice(mContext, profileManager, device);
mCsipDeviceManager.initCsipDeviceIfNeeded(newDevice);
mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(newDevice, leScanFilters);
if (!mCsipDeviceManager.setMemberDeviceIfNeeded(newDevice)
&& !mHearingAidDeviceManager.setSubDeviceIfNeeded(newDevice)) {
mCachedDevices.add(newDevice);
mBtManager.getEventManager().dispatchDeviceAdded(newDevice);
}
}
}
return newDevice;
}
/**
* Returns device summary of the pair of the hearing aid / CSIP passed as the parameter.
*
* @param CachedBluetoothDevice device
* @return Device summary, or if the pair does not exist or if it is not a hearing aid or
* a CSIP set member, then {@code null}.
*/
public synchronized String getSubDeviceSummary(CachedBluetoothDevice device) {
final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice();
// TODO: check the CSIP group size instead of the real member device set size, and adjust
// the size restriction.
if (!memberDevices.isEmpty()) {
for (CachedBluetoothDevice memberDevice : memberDevices) {
if (memberDevice.isConnected()) {
return memberDevice.getConnectionSummary();
}
}
}
CachedBluetoothDevice subDevice = device.getSubDevice();
if (subDevice != null && subDevice.isConnected()) {
return subDevice.getConnectionSummary();
}
return null;
}
/**
* Search for existing sub device {@link CachedBluetoothDevice}.
*
* @param device the address of the Bluetooth device
* @return true for found sub / member device or false.
*/
public synchronized boolean isSubDevice(BluetoothDevice device) {
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
if (!cachedDevice.getDevice().equals(device)) {
// Check the member devices of the coordinated set if it exists
Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
if (!memberDevices.isEmpty()) {
for (CachedBluetoothDevice memberDevice : memberDevices) {
if (memberDevice.getDevice().equals(device)) {
return true;
}
}
continue;
}
// Check sub devices of hearing aid if it exists
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null && subDevice.getDevice().equals(device)) {
return true;
}
}
}
return false;
}
/**
* Updates the Hearing Aid devices; specifically the HiSyncId's. This routine is called when the
* Hearing Aid Service is connected and the HiSyncId's are now available.
*/
public synchronized void updateHearingAidsDevices() {
mHearingAidDeviceManager.updateHearingAidsDevices();
}
/**
* Updates the Csip devices; specifically the GroupId's. This routine is called when the
* CSIS is connected and the GroupId's are now available.
*/
public synchronized void updateCsipDevices() {
mCsipDeviceManager.updateCsipDevices();
}
/**
* Attempts to get the name of a remote device, otherwise returns the address.
*
* @param device The remote device.
* @return The name, or if unavailable, the address.
*/
public String getName(BluetoothDevice device) {
if (isOngoingPairByCsip(device)) {
CachedBluetoothDevice firstDevice =
mCsipDeviceManager.getFirstMemberDevice(mGroupIdOfLateBonding);
if (firstDevice != null && firstDevice.getName() != null) {
return firstDevice.getName();
}
}
CachedBluetoothDevice cachedDevice = findDevice(device);
if (cachedDevice != null && cachedDevice.getName() != null) {
return cachedDevice.getName();
}
String name = device.getAlias();
if (name != null) {
return name;
}
return device.getAddress();
}
public synchronized void clearNonBondedDevices() {
clearNonBondedSubDevices();
final List<CachedBluetoothDevice> removedCachedDevice = new ArrayList<>();
mCachedDevices.stream()
.filter(cachedDevice -> cachedDevice.getBondState() == BluetoothDevice.BOND_NONE)
.forEach(cachedDevice -> {
cachedDevice.release();
removedCachedDevice.add(cachedDevice);
});
mCachedDevices.removeAll(removedCachedDevice);
}
private void clearNonBondedSubDevices() {
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
if (!memberDevices.isEmpty()) {
for (Object it : memberDevices.toArray()) {
CachedBluetoothDevice memberDevice = (CachedBluetoothDevice) it;
// Member device exists and it is not bonded
if (memberDevice.getDevice().getBondState() == BluetoothDevice.BOND_NONE) {
cachedDevice.removeMemberDevice(memberDevice);
}
}
return;
}
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null
&& subDevice.getDevice().getBondState() == BluetoothDevice.BOND_NONE) {
// Sub device exists and it is not bonded
subDevice.release();
cachedDevice.setSubDevice(null);
}
}
}
public synchronized void onScanningStateChanged(boolean started) {
if (!started) return;
// If starting a new scan, clear old visibility
// Iterate in reverse order since devices may be removed.
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
cachedDevice.setJustDiscovered(false);
final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
if (!memberDevices.isEmpty()) {
for (CachedBluetoothDevice memberDevice : memberDevices) {
memberDevice.setJustDiscovered(false);
}
return;
}
final CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null) {
subDevice.setJustDiscovered(false);
}
}
}
public synchronized void onBluetoothStateChanged(int bluetoothState) {
// When Bluetooth is turning off, we need to clear the non-bonded devices
// Otherwise, they end up showing up on the next BT enable
if (bluetoothState == BluetoothAdapter.STATE_TURNING_OFF) {
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
final Set<CachedBluetoothDevice> memberDevices = cachedDevice.getMemberDevice();
if (!memberDevices.isEmpty()) {
for (CachedBluetoothDevice memberDevice : memberDevices) {
if (memberDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
cachedDevice.removeMemberDevice(memberDevice);
}
}
} else {
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null) {
if (subDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
cachedDevice.setSubDevice(null);
}
}
}
if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) {
cachedDevice.setJustDiscovered(false);
cachedDevice.release();
mCachedDevices.remove(i);
}
}
// To clear the SetMemberPair flag when the Bluetooth is turning off.
mOngoingSetMemberPair = null;
mIsLateBonding = false;
mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
}
synchronized void removeDuplicateInstanceForIdentityAddress(BluetoothDevice device) {
String identityAddress = device.getIdentityAddress();
if (identityAddress == null || identityAddress.equals(device.getAddress())) {
return;
}
mCachedDevices.removeIf(d -> {
boolean shouldRemove = d.getDevice().getAddress().equals(identityAddress);
if (shouldRemove) {
Log.d(TAG, "Remove instance for identity address " + d);
}
return shouldRemove;
});
}
public synchronized boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice
cachedDevice, int state, int profileId) {
if (profileId == BluetoothProfile.HEARING_AID) {
return mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice,
state);
}
if (profileId == BluetoothProfile.HEADSET
|| profileId == BluetoothProfile.A2DP
|| profileId == BluetoothProfile.LE_AUDIO
|| profileId == BluetoothProfile.CSIP_SET_COORDINATOR) {
return mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice,
state);
}
return false;
}
/** Handles when the device been set as active/inactive. */
public synchronized void onActiveDeviceChanged(CachedBluetoothDevice cachedBluetoothDevice) {
if (cachedBluetoothDevice.isHearingAidDevice()) {
mHearingAidDeviceManager.onActiveDeviceChanged(cachedBluetoothDevice);
}
}
public synchronized void onDeviceUnpaired(CachedBluetoothDevice device) {
device.setGroupId(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(device);
// Should iterate through the cloned set to avoid ConcurrentModificationException
final Set<CachedBluetoothDevice> memberDevices = new HashSet<>(device.getMemberDevice());
if (!memberDevices.isEmpty()) {
// Main device is unpaired, also unpair the member devices
for (CachedBluetoothDevice memberDevice : memberDevices) {
memberDevice.unpair();
memberDevice.setGroupId(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
device.removeMemberDevice(memberDevice);
}
} else if (mainDevice != null) {
// Member device is unpaired, also unpair the main device
mainDevice.unpair();
}
mainDevice = mHearingAidDeviceManager.findMainDevice(device);
CachedBluetoothDevice subDevice = device.getSubDevice();
if (subDevice != null) {
// Main device is unpaired, to unpair sub device
subDevice.unpair();
device.setSubDevice(null);
} else if (mainDevice != null) {
// Sub device unpaired, to unpair main device
mainDevice.unpair();
mainDevice.setSubDevice(null);
}
}
/**
* Called when we found a set member of a group. The function will check the {@code groupId} if
* it exists and the bond state of the device is BOND_NOE, and if there isn't any ongoing pair
* , and then return {@code true} to pair the device automatically.
*
* @param device The found device
* @param groupId The group id of the found device
*
* @return {@code true}, if the device should pair automatically; Otherwise, return
* {@code false}.
*/
private synchronized boolean shouldPairByCsip(BluetoothDevice device, int groupId) {
boolean isOngoingSetMemberPair = mOngoingSetMemberPair != null;
int bondState = device.getBondState();
boolean groupExists = mCsipDeviceManager.isExistedGroupId(groupId);
Log.d(TAG,
"isOngoingSetMemberPair=" + isOngoingSetMemberPair + ", bondState=" + bondState
+ ", groupExists=" + groupExists + ", groupId=" + groupId);
if (isOngoingSetMemberPair || bondState != BluetoothDevice.BOND_NONE || !groupExists) {
return false;
}
return true;
}
private synchronized boolean checkLateBonding(int groupId) {
CachedBluetoothDevice firstDevice = mCsipDeviceManager.getFirstMemberDevice(groupId);
if (firstDevice == null) {
Log.d(TAG, "No first device in group: " + groupId);
return false;
}
Timestamp then = firstDevice.getBondTimestamp();
if (then == null) {
Log.d(TAG, "No bond timestamp");
return true;
}
Timestamp now = new Timestamp(System.currentTimeMillis());
long diff = (now.getTime() - then.getTime());
Log.d(TAG, "Time difference to first bonding: " + diff + "ms");
return diff > sLateBondingTimeoutMillis;
}
/**
* Called to check if there is an ongoing bonding for the device and it is late bonding.
* If the device is not matching the ongoing bonding device then false will be returned.
*
* @param device The device to check.
*/
public synchronized boolean isLateBonding(BluetoothDevice device) {
if (!isOngoingPairByCsip(device)) {
Log.d(TAG, "isLateBonding: pair not ongoing or not matching device");
return false;
}
Log.d(TAG, "isLateBonding: " + mIsLateBonding);
return mIsLateBonding;
}
/**
* Called when we found a set member of a group. The function will check the {@code groupId} if
* it exists and the bond state of the device is BOND_NONE, and if there isn't any ongoing pair
* , and then pair the device automatically.
*
* @param device The found device
* @param groupId The group id of the found device
*/
public synchronized void pairDeviceByCsip(BluetoothDevice device, int groupId) {
if (!shouldPairByCsip(device, groupId)) {
return;
}
Log.d(TAG, "Bond " + device.getAnonymizedAddress() + " groupId=" + groupId + " by CSIP ");
mOngoingSetMemberPair = device;
mIsLateBonding = checkLateBonding(groupId);
mGroupIdOfLateBonding = groupId;
syncConfigFromMainDevice(device, groupId);
if (!device.createBond(BluetoothDevice.TRANSPORT_LE)) {
Log.d(TAG, "Bonding could not be started");
mOngoingSetMemberPair = null;
mIsLateBonding = false;
mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
}
private void syncConfigFromMainDevice(BluetoothDevice device, int groupId) {
if (!isOngoingPairByCsip(device)) {
return;
}
CachedBluetoothDevice memberDevice = findDevice(device);
CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(memberDevice);
if (mainDevice == null) {
mainDevice = mCsipDeviceManager.getCachedDevice(groupId);
}
if (mainDevice == null || mainDevice.equals(memberDevice)) {
Log.d(TAG, "no mainDevice");
return;
}
// The memberDevice set PhonebookAccessPermission
device.setPhonebookAccessPermission(mainDevice.getDevice().getPhonebookAccessPermission());
}
/**
* Called when the bond state change. If the bond state change is related with the
* ongoing set member pair, the cachedBluetoothDevice will be created but the UI
* would not be updated. For the other case, return {@code false} to go through the normal
* flow.
*
* @param device The device
* @param bondState The new bond state
*
* @return {@code true}, if the bond state change for the device is handled inside this
* function, and would not like to update the UI. If not, return {@code false}.
*/
public synchronized boolean onBondStateChangedIfProcess(BluetoothDevice device, int bondState) {
if (!isOngoingPairByCsip(device)) {
return false;
}
if (bondState == BluetoothDevice.BOND_BONDING) {
return true;
}
mOngoingSetMemberPair = null;
mIsLateBonding = false;
mGroupIdOfLateBonding = BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
if (bondState != BluetoothDevice.BOND_NONE) {
if (findDevice(device) == null) {
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
CachedBluetoothDevice newDevice =
new CachedBluetoothDevice(mContext, profileManager, device);
mCachedDevices.add(newDevice);
findDevice(device).connect();
}
}
return true;
}
/**
* Check if the device is the one which is initial paired locally by CSIP. The setting
* would depned on it to accept the pairing request automatically
*
* @param device The device
*
* @return {@code true}, if the device is ongoing pair by CSIP. Otherwise, return
* {@code false}.
*/
public boolean isOngoingPairByCsip(BluetoothDevice device) {
return mOngoingSetMemberPair != null && mOngoingSetMemberPair.equals(device);
}
private void log(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
}
}
}

View File

@@ -0,0 +1,401 @@
/*
* Copyright (C) 2021 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.settingslib.bluetooth;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.os.Build;
import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.ChecksSdkIntAtLeast;
import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* CsipDeviceManager manages the set of remote CSIP Bluetooth devices.
*/
public class CsipDeviceManager {
private static final String TAG = "CsipDeviceManager";
private static final boolean DEBUG = BluetoothUtils.D;
private final LocalBluetoothManager mBtManager;
private final List<CachedBluetoothDevice> mCachedDevices;
CsipDeviceManager(LocalBluetoothManager localBtManager,
List<CachedBluetoothDevice> cachedDevices) {
mBtManager = localBtManager;
mCachedDevices = cachedDevices;
}
void initCsipDeviceIfNeeded(CachedBluetoothDevice newDevice) {
// Current it only supports the base uuid for CSIP and group this set in UI.
final int groupId = getBaseGroupId(newDevice.getDevice());
if (isValidGroupId(groupId)) {
log("initCsipDeviceIfNeeded: " + newDevice + " (group: " + groupId + ")");
// Once groupId is valid, assign groupId
newDevice.setGroupId(groupId);
}
}
private int getBaseGroupId(BluetoothDevice device) {
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
final CsipSetCoordinatorProfile profileProxy = profileManager
.getCsipSetCoordinatorProfile();
if (profileProxy != null) {
final Map<Integer, ParcelUuid> groupIdMap = profileProxy
.getGroupUuidMapByDevice(device);
if (groupIdMap == null) {
return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
for (Map.Entry<Integer, ParcelUuid> entry : groupIdMap.entrySet()) {
if (entry.getValue().equals(BluetoothUuid.CAP)) {
return entry.getKey();
}
}
}
return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
boolean setMemberDeviceIfNeeded(CachedBluetoothDevice newDevice) {
final int groupId = newDevice.getGroupId();
if (isValidGroupId(groupId)) {
final CachedBluetoothDevice mainDevice = getCachedDevice(groupId);
log("setMemberDeviceIfNeeded, main: " + mainDevice + ", member: " + newDevice);
// Just add one of the coordinated set from a pair in the list that is shown in the UI.
// Once there is other devices with the same groupId, to add new device as member
// devices.
if (mainDevice != null) {
mainDevice.addMemberDevice(newDevice);
newDevice.setName(mainDevice.getName());
return true;
}
}
return false;
}
private boolean isValidGroupId(int groupId) {
return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
/**
* To find the device with {@code groupId}.
*
* @param groupId The group id
* @return if we could find a device with this {@code groupId} return this device. Otherwise,
* return null.
*/
public CachedBluetoothDevice getCachedDevice(int groupId) {
log("getCachedDevice: groupId: " + groupId);
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
if (cachedDevice.getGroupId() == groupId) {
log("getCachedDevice: found cachedDevice with the groupId: "
+ cachedDevice.getDevice().getAnonymizedAddress());
return cachedDevice;
}
}
return null;
}
// To collect all set member devices and call #onGroupIdChanged to group device by GroupId
void updateCsipDevices() {
final Set<Integer> newGroupIdSet = new HashSet<Integer>();
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
// Do nothing if GroupId has been assigned
if (!isValidGroupId(cachedDevice.getGroupId())) {
final int newGroupId = getBaseGroupId(cachedDevice.getDevice());
// Do nothing if there is no GroupId on Bluetooth device
if (isValidGroupId(newGroupId)) {
cachedDevice.setGroupId(newGroupId);
newGroupIdSet.add(newGroupId);
}
}
}
for (int groupId : newGroupIdSet) {
onGroupIdChanged(groupId);
}
}
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
private static boolean isAtLeastT() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
}
// Group devices by groupId
@VisibleForTesting
void onGroupIdChanged(int groupId) {
if (!isValidGroupId(groupId)) {
log("onGroupIdChanged: groupId is invalid");
return;
}
updateRelationshipOfGroupDevices(groupId);
}
// @return {@code true}, the event is processed inside the method. It is for updating
// le audio device on group relationship when receiving connected or disconnected.
// @return {@code false}, it is not le audio device or to process it same as other profiles
boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice,
int state) {
log("onProfileConnectionStateChangedIfProcessed: " + cachedDevice + ", state: " + state);
if (state != BluetoothProfile.STATE_CONNECTED
&& state != BluetoothProfile.STATE_DISCONNECTED) {
return false;
}
return updateRelationshipOfGroupDevices(cachedDevice.getGroupId());
}
@VisibleForTesting
boolean updateRelationshipOfGroupDevices(int groupId) {
if (!isValidGroupId(groupId)) {
log("The device is not group.");
return false;
}
log("updateRelationshipOfGroupDevices: mCachedDevices list =" + mCachedDevices.toString());
// Get the preferred main device by getPreferredMainDeviceWithoutConectionState
List<CachedBluetoothDevice> groupDevicesList = getGroupDevicesFromAllOfDevicesList(groupId);
CachedBluetoothDevice preferredMainDevice =
getPreferredMainDevice(groupId, groupDevicesList);
log("The preferredMainDevice= " + preferredMainDevice
+ " and the groupDevicesList of groupId= " + groupId
+ " =" + groupDevicesList);
return addMemberDevicesIntoMainDevice(groupId, preferredMainDevice);
}
CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) {
if (device == null || mCachedDevices == null) {
return null;
}
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
if (isValidGroupId(cachedDevice.getGroupId())) {
Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice();
if (memberSet.isEmpty()) {
continue;
}
for (CachedBluetoothDevice memberDevice : memberSet) {
if (memberDevice != null && memberDevice.equals(device)) {
return cachedDevice;
}
}
}
}
return null;
}
/**
* Check if the {@code groupId} is existed.
*
* @param groupId The group id
* @return {@code true}, if we could find a device with this {@code groupId}; Otherwise,
* return {@code false}.
*/
public boolean isExistedGroupId(int groupId) {
return getCachedDevice(groupId) != null;
}
@VisibleForTesting
List<CachedBluetoothDevice> getGroupDevicesFromAllOfDevicesList(int groupId) {
List<CachedBluetoothDevice> groupDevicesList = new ArrayList<>();
if (!isValidGroupId(groupId)) {
return groupDevicesList;
}
for (CachedBluetoothDevice item : mCachedDevices) {
if (groupId != item.getGroupId()) {
continue;
}
groupDevicesList.add(item);
groupDevicesList.addAll(item.getMemberDevice());
}
return groupDevicesList;
}
public CachedBluetoothDevice getFirstMemberDevice(int groupId) {
List<CachedBluetoothDevice> members = getGroupDevicesFromAllOfDevicesList(groupId);
if (members.isEmpty())
return null;
CachedBluetoothDevice firstMember = members.get(0);
log("getFirstMemberDevice: groupId=" + groupId
+ " address=" + firstMember.getDevice().getAnonymizedAddress());
return firstMember;
}
@VisibleForTesting
CachedBluetoothDevice getPreferredMainDevice(int groupId,
List<CachedBluetoothDevice> groupDevicesList) {
// How to select the preferred main device?
// 1. The DUAL mode connected device which has A2DP/HFP and LE audio.
// 2. One of connected LE device in the list. Default is the lead device from LE profile.
// 3. If there is no connected device, then reset the relationship. Set the DUAL mode
// deviced as the main device. Otherwise, set any one of the device.
if (groupDevicesList == null || groupDevicesList.isEmpty()) {
return null;
}
CachedBluetoothDevice dualModeDevice = groupDevicesList.stream()
.filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream()
.anyMatch(profile -> profile instanceof LeAudioProfile))
.filter(cachedDevice -> cachedDevice.getConnectableProfiles().stream()
.anyMatch(profile -> profile instanceof A2dpProfile
|| profile instanceof HeadsetProfile))
.findFirst().orElse(null);
if (isDeviceConnected(dualModeDevice)) {
log("getPreferredMainDevice: The connected DUAL mode device");
return dualModeDevice;
}
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile();
final BluetoothDevice leAudioLeadDevice = (leAudioProfile != null && isAtLeastT())
? leAudioProfile.getConnectedGroupLeadDevice(groupId) : null;
if (leAudioLeadDevice != null) {
log("getPreferredMainDevice: The LeadDevice from LE profile is "
+ leAudioLeadDevice.getAnonymizedAddress());
}
CachedBluetoothDevice leAudioLeadCachedDevice =
leAudioLeadDevice != null ? deviceManager.findDevice(leAudioLeadDevice) : null;
if (leAudioLeadCachedDevice == null) {
log("getPreferredMainDevice: The LeadDevice is not in the all of devices list");
} else if (isDeviceConnected(leAudioLeadCachedDevice)) {
log("getPreferredMainDevice: The connected LeadDevice from LE profile");
return leAudioLeadCachedDevice;
}
CachedBluetoothDevice oneOfConnectedDevices =
groupDevicesList.stream()
.filter(cachedDevice -> isDeviceConnected(cachedDevice))
.findFirst()
.orElse(null);
if (oneOfConnectedDevices != null) {
log("getPreferredMainDevice: One of the connected devices.");
return oneOfConnectedDevices;
}
if (dualModeDevice != null) {
log("getPreferredMainDevice: The DUAL mode device.");
return dualModeDevice;
}
// last
if (!groupDevicesList.isEmpty()) {
log("getPreferredMainDevice: One of the group devices.");
return groupDevicesList.get(0);
}
return null;
}
@VisibleForTesting
boolean addMemberDevicesIntoMainDevice(int groupId, CachedBluetoothDevice preferredMainDevice) {
boolean hasChanged = false;
if (preferredMainDevice == null) {
log("addMemberDevicesIntoMainDevice: No main device. Do nothing.");
return hasChanged;
}
// If the current main device is not preferred main device, then set it as new main device.
// Otherwise, do nothing.
BluetoothDevice bluetoothDeviceOfPreferredMainDevice = preferredMainDevice.getDevice();
CachedBluetoothDevice mainDeviceOfPreferredMainDevice = findMainDevice(preferredMainDevice);
boolean hasPreferredMainDeviceAlreadyBeenMainDevice =
mainDeviceOfPreferredMainDevice == null;
if (!hasPreferredMainDeviceAlreadyBeenMainDevice) {
// preferredMainDevice has not been the main device.
// switch relationship between the mainDeviceOfPreferredMainDevice and
// PreferredMainDevice
log("addMemberDevicesIntoMainDevice: The PreferredMainDevice have the mainDevice. "
+ "Do switch relationship between the mainDeviceOfPreferredMainDevice and "
+ "PreferredMainDevice");
// To switch content and dispatch to notify UI change
mBtManager.getEventManager().dispatchDeviceRemoved(mainDeviceOfPreferredMainDevice);
mainDeviceOfPreferredMainDevice.switchMemberDeviceContent(preferredMainDevice);
mainDeviceOfPreferredMainDevice.refresh();
// It is necessary to do remove and add for updating the mapping on
// preference and device
mBtManager.getEventManager().dispatchDeviceAdded(mainDeviceOfPreferredMainDevice);
hasChanged = true;
}
// If the mCachedDevices List at CachedBluetoothDeviceManager has multiple items which are
// the same groupId, then combine them and also keep the preferred main device as main
// device.
List<CachedBluetoothDevice> topLevelOfGroupDevicesList = mCachedDevices.stream()
.filter(device -> device.getGroupId() == groupId)
.collect(Collectors.toList());
boolean haveMultiMainDevicesInAllOfDevicesList = topLevelOfGroupDevicesList.size() > 1;
// Update the new main of CachedBluetoothDevice, since it may be changed in above step.
final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
preferredMainDevice = deviceManager.findDevice(bluetoothDeviceOfPreferredMainDevice);
if (haveMultiMainDevicesInAllOfDevicesList) {
// put another devices into main device.
for (CachedBluetoothDevice deviceItem : topLevelOfGroupDevicesList) {
if (deviceItem.getDevice() == null || deviceItem.getDevice().equals(
bluetoothDeviceOfPreferredMainDevice)) {
continue;
}
Set<CachedBluetoothDevice> memberSet = deviceItem.getMemberDevice();
for (CachedBluetoothDevice memberSetItem : memberSet) {
if (!memberSetItem.equals(preferredMainDevice)) {
preferredMainDevice.addMemberDevice(memberSetItem);
}
}
memberSet.clear();
preferredMainDevice.addMemberDevice(deviceItem);
mCachedDevices.remove(deviceItem);
mBtManager.getEventManager().dispatchDeviceRemoved(deviceItem);
hasChanged = true;
}
}
if (hasChanged) {
log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: "
+ mCachedDevices);
}
return hasChanged;
}
private void log(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
}
}
private boolean isDeviceConnected(CachedBluetoothDevice cachedDevice) {
if (cachedDevice == null) {
return false;
}
final BluetoothDevice device = cachedDevice.getDevice();
return cachedDevice.isConnected()
&& device.getBondState() == BluetoothDevice.BOND_BONDED
&& device.isConnected();
}
}

View File

@@ -0,0 +1,251 @@
/*
* Copyright 2021 HIMSA II K/S - www.himsa.com.
* Represented by EHIMA - www.ehima.com
*
* 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.RequiresApi;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* CSIP Set Coordinator handles Bluetooth CSIP Set Coordinator role profile.
*/
public class CsipSetCoordinatorProfile implements LocalBluetoothProfile {
private static final String TAG = "CsipSetCoordinatorProfile";
private static final boolean VDBG = true;
private Context mContext;
private BluetoothCsipSetCoordinator mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
static final String NAME = "CSIP Set Coordinator";
private final LocalBluetoothProfileManager mProfileManager;
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
// These callbacks run on the main thread.
private final class CoordinatedSetServiceListener implements BluetoothProfile.ServiceListener {
@RequiresApi(33)
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (VDBG) {
Log.d(TAG, "Bluetooth service connected");
}
mService = (BluetoothCsipSetCoordinator) proxy;
// We just bound to the service, so refresh the UI for any connected CSIP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
if (VDBG) {
Log.d(TAG, "CsipSetCoordinatorProfile found new device: " + nextDevice);
}
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(
CsipSetCoordinatorProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mDeviceManager.updateCsipDevices();
mProfileManager.callServiceConnectedListeners();
mIsProfileReady = true;
}
public void onServiceDisconnected(int profile) {
if (VDBG) {
Log.d(TAG, "Bluetooth service disconnected");
}
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady = false;
}
}
CsipSetCoordinatorProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mContext = context;
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context,
new CoordinatedSetServiceListener(), BluetoothProfile.CSIP_SET_COORDINATOR);
}
/**
* Get CSIP devices matching connection states{
*
* @code BluetoothProfile.STATE_CONNECTED,
* @code BluetoothProfile.STATE_CONNECTING,
* @code BluetoothProfile.STATE_DISCONNECTING}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED, BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
/**
* Gets the connection status of the device.
*
* @code BluetoothProfile.STATE_CONNECTED,
* @code BluetoothProfile.STATE_CONNECTING,
* @code BluetoothProfile.STATE_DISCONNECTING}
*
* @return Connection status, {@code BluetoothProfile.STATE_DISCONNECTED} if unknown.
*/
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.CSIP_SET_COORDINATOR;
}
@Override
public boolean accessProfileEnabled() {
return false;
}
@Override
public boolean isAutoConnectable() {
return true;
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null || device == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null || device == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null || device == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
@Override
public int getOrdinal() {
return ORDINAL;
}
@Override
public int getNameResource(BluetoothDevice device) {
return R.string.summary_empty;
}
@Override
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
return BluetoothUtils.getConnectionStateSummary(state);
}
@Override
public int getDrawableResource(BluetoothClass btClass) {
return 0;
}
/**
* Get the device's groups and correspondsing uuids map.
* @param device the bluetooth device
* @return Map of groups ids and related UUIDs
*/
public Map<Integer, ParcelUuid> getGroupUuidMapByDevice(BluetoothDevice device) {
if (mService == null || device == null) {
return null;
}
return mService.getGroupUuidMapByDevice(device);
}
/**
* Return the profile name as a string.
*/
public String toString() {
return NAME;
}
@RequiresApi(33)
protected void finalize() {
if (VDBG) {
Log.d(TAG, "finalize()");
}
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(
BluetoothProfile.CSIP_SET_COORDINATOR, mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up CSIP Set Coordinator proxy", t);
}
}
}
}

View File

@@ -0,0 +1,654 @@
/*
* 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHapClient;
import android.bluetooth.BluetoothHapPresetInfo;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settingslib.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* HapClientProfile handles the Bluetooth HAP service client role.
*/
public class HapClientProfile implements LocalBluetoothProfile {
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {
HearingAidType.TYPE_INVALID,
HearingAidType.TYPE_BINAURAL,
HearingAidType.TYPE_MONAURAL,
HearingAidType.TYPE_BANDED,
HearingAidType.TYPE_RFU
})
/** Hearing aid type definition for HAP Client. */
public @interface HearingAidType {
int TYPE_INVALID = -1;
int TYPE_BINAURAL = BluetoothHapClient.TYPE_BINAURAL;
int TYPE_MONAURAL = BluetoothHapClient.TYPE_MONAURAL;
int TYPE_BANDED = BluetoothHapClient.TYPE_BANDED;
int TYPE_RFU = BluetoothHapClient.TYPE_RFU;
}
static final String NAME = "HapClient";
private static final String TAG = "HapClientProfile";
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
private final BluetoothAdapter mBluetoothAdapter;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
private BluetoothHapClient mService;
private boolean mIsProfileReady;
// These callbacks run on the main thread.
private final class HapClientServiceListener implements BluetoothProfile.ServiceListener {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothHapClient) proxy;
// We just bound to the service, so refresh the UI for any connected HapClient devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// Adds a new device into mDeviceManager if it does not exist
if (device == null) {
Log.w(TAG, "HapClient profile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(
HapClientProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
// Check current list of CachedDevices to see if any are hearing aid devices.
mDeviceManager.updateHearingAidsDevices();
mIsProfileReady = true;
mProfileManager.callServiceConnectedListeners();
}
@Override
public void onServiceDisconnected(int profile) {
mIsProfileReady = false;
mProfileManager.callServiceDisconnectedListeners();
}
}
HapClientProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class);
if (bluetoothManager != null) {
mBluetoothAdapter = bluetoothManager.getAdapter();
mBluetoothAdapter.getProfileProxy(context, new HapClientServiceListener(),
BluetoothProfile.HAP_CLIENT);
} else {
mBluetoothAdapter = null;
}
}
/**
* Registers a {@link BluetoothHapClient.Callback} that will be invoked during the
* operation of this profile.
*
* Repeated registration of the same <var>callback</var> object after the first call to this
* method will result with IllegalArgumentException being thrown, even when the
* <var>executor</var> is different. API caller would have to call
* {@link #unregisterCallback(BluetoothHapClient.Callback)} with the same callback object
* before registering it again.
*
* @param executor an {@link Executor} to execute given callback
* @param callback user implementation of the {@link BluetoothHapClient.Callback}
* @throws NullPointerException if a null executor, or callback is given, or
* IllegalArgumentException if the same <var>callback</var> is already registered.
* @hide
*/
public void registerCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull BluetoothHapClient.Callback callback) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot register callback.");
return;
}
mService.registerCallback(executor, callback);
}
/**
* Unregisters the specified {@link BluetoothHapClient.Callback}.
* <p>The same {@link BluetoothHapClient.Callback} object used when calling
* {@link #registerCallback(Executor, BluetoothHapClient.Callback)} must be used.
*
* <p>Callbacks are automatically unregistered when application process goes away
*
* @param callback user implementation of the {@link BluetoothHapClient.Callback}
* @throws NullPointerException when callback is null or IllegalArgumentException when no
* callback is registered
* @hide
*/
public void unregisterCallback(@NonNull BluetoothHapClient.Callback callback) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot unregister callback.");
return;
}
mService.unregisterCallback(callback);
}
/**
* Gets hearing aid devices matching connection states{
* {@code BluetoothProfile.STATE_CONNECTED},
* {@code BluetoothProfile.STATE_CONNECTING},
* {@code BluetoothProfile.STATE_DISCONNECTING}}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectedDevices() {
return getDevicesByStates(new int[] {
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
/**
* Gets hearing aid devices matching connection states{
* {@code BluetoothProfile.STATE_DISCONNECTED},
* {@code BluetoothProfile.STATE_CONNECTED},
* {@code BluetoothProfile.STATE_CONNECTING},
* {@code BluetoothProfile.STATE_DISCONNECTING}}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectableDevices() {
return getDevicesByStates(new int[] {
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
private List<BluetoothDevice> getDevicesByStates(int[] states) {
if (mService == null) {
return new ArrayList<>(0);
}
return mService.getDevicesMatchingConnectionStates(states);
}
/**
* Gets the hearing aid type of the device.
*
* @param device is the device for which we want to get the hearing aid type
* @return hearing aid type
*/
@HearingAidType
public int getHearingAidType(@NonNull BluetoothDevice device) {
if (mService == null) {
return HearingAidType.TYPE_INVALID;
}
return mService.getHearingAidType(device);
}
/**
* Gets if this device supports synchronized presets or not
*
* @param device is the device for which we want to know if supports synchronized presets
* @return {@code true} if the device supports synchronized presets
*/
public boolean supportsSynchronizedPresets(@NonNull BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.supportsSynchronizedPresets(device);
}
/**
* Gets if this device supports independent presets or not
*
* @param device is the device for which we want to know if supports independent presets
* @return {@code true} if the device supports independent presets
*/
public boolean supportsIndependentPresets(@NonNull BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.supportsIndependentPresets(device);
}
/**
* Gets if this device supports dynamic presets or not
*
* @param device is the device for which we want to know if supports dynamic presets
* @return {@code true} if the device supports dynamic presets
*/
public boolean supportsDynamicPresets(@NonNull BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.supportsDynamicPresets(device);
}
/**
* Gets if this device supports writable presets or not
*
* @param device is the device for which we want to know if supports writable presets
* @return {@code true} if the device supports writable presets
*/
public boolean supportsWritablePresets(@NonNull BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.supportsWritablePresets(device);
}
/**
* Gets the group identifier, which can be used in the group related part of the API.
*
* <p>Users are expected to get group identifier for each of the connected device to discover
* the device grouping. This allows them to make an informed decision which devices can be
* controlled by single group API call and which require individual device calls.
*
* <p>Note that some binaural HA devices may not support group operations, therefore are not
* considered a valid HAP group. In such case -1 is returned even if such device is a valid Le
* Audio Coordinated Set member.
*
* @param device is the device for which we want to get the hap group identifier
* @return valid group identifier or -1
* @hide
*/
public int getHapGroup(@NonNull BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot get hap group.");
return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
return mService.getHapGroup(device);
}
/**
* Gets the currently active preset for a HA device.
*
* @param device is the device for which we want to set the active preset
* @return active preset index or {@link BluetoothHapClient#PRESET_INDEX_UNAVAILABLE} if the
* device is not connected.
* @hide
*/
public int getActivePresetIndex(@NonNull BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot get active preset index.");
return BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
}
return mService.getActivePresetIndex(device);
}
/**
* Gets the currently active preset info for a remote device.
*
* @param device is the device for which we want to get the preset name
* @return currently active preset info if selected, null if preset info is not available for
* the remote device
* @hide
*/
@Nullable
public BluetoothHapPresetInfo getActivePresetInfo(@NonNull BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot get active preset info.");
return null;
}
return mService.getActivePresetInfo(device);
}
/**
* Selects the currently active preset for a HA device
*
* <p>On success,
* {@link BluetoothHapClient.Callback#onPresetSelected(BluetoothDevice, int, int)} will be
* called with reason code {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure,
* {@link BluetoothHapClient.Callback#onPresetSelectionFailed(BluetoothDevice, int)} will be
* called.
*
* @param device is the device for which we want to set the active preset
* @param presetIndex is an index of one of the available presets
* @hide
*/
public void selectPreset(@NonNull BluetoothDevice device, int presetIndex) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot select preset.");
return;
}
mService.selectPreset(device, presetIndex);
}
/**
* Selects the currently active preset for a Hearing Aid device group.
*
* <p>This group call may replace multiple device calls if those are part of the valid HAS
* group. Note that binaural HA devices may or may not support group.
*
* <p>On success,
* {@link BluetoothHapClient.Callback#onPresetSelected(BluetoothDevice, int, int)} will be
* called for each device within the group with reason code
* {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure,
* {@link BluetoothHapClient.Callback#onPresetSelectionForGroupFailed(int, int)} will be
* called for the group.
*
* @param groupId is the device group identifier for which want to set the active preset
* @param presetIndex is an index of one of the available presets
* @hide
*/
public void selectPresetForGroup(int groupId, int presetIndex) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot select preset for group.");
return;
}
mService.selectPresetForGroup(groupId, presetIndex);
}
/**
* Sets the next preset as a currently active preset for a HA device
*
* <p>Note that the meaning of 'next' is HA device implementation specific and does not
* necessarily mean a higher preset index.
*
* @param device is the device for which we want to set the active preset
* @hide
*/
public void switchToNextPreset(@NonNull BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot switch to next preset.");
return;
}
mService.switchToNextPreset(device);
}
/**
* Sets the next preset as a currently active preset for a HA device group
*
* <p>Note that the meaning of 'next' is HA device implementation specific and does not
* necessarily mean a higher preset index.
*
* <p>This group call may replace multiple device calls if those are part of the valid HAS
* group. Note that binaural HA devices may or may not support group.
*
* @param groupId is the device group identifier for which want to set the active preset
* @hide
*/
public void switchToNextPresetForGroup(int groupId) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot switch to next preset for group.");
return;
}
mService.switchToNextPresetForGroup(groupId);
}
/**
* Sets the previous preset as a currently active preset for a HA device.
*
* <p>Note that the meaning of 'previous' is HA device implementation specific and does not
* necessarily mean a lower preset index.
*
* @param device is the device for which we want to set the active preset
* @hide
*/
public void switchToPreviousPreset(@NonNull BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot switch to previous preset.");
return;
}
mService.switchToPreviousPreset(device);
}
/**
* Sets the next preset as a currently active preset for a HA device group
*
* <p>Note that the meaning of 'next' is HA device implementation specific and does not
* necessarily mean a higher preset index.
*
* <p>This group call may replace multiple device calls if those are part of the valid HAS
* group. Note that binaural HA devices may or may not support group.
*
* @param groupId is the device group identifier for which want to set the active preset
* @hide
*/
public void switchToPreviousPresetForGroup(int groupId) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot switch to previous preset for "
+ "group.");
return;
}
mService.switchToPreviousPresetForGroup(groupId);
}
/**
* Requests the preset info
*
* @param device is the device for which we want to get the preset name
* @param presetIndex is an index of one of the available presets
* @return preset info
* @hide
*/
public BluetoothHapPresetInfo getPresetInfo(@NonNull BluetoothDevice device, int presetIndex) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot get preset info.");
return null;
}
return mService.getPresetInfo(device, presetIndex);
}
/**
* Gets all preset info for a particular device
*
* @param device is the device for which we want to get all presets info
* @return a list of all known preset info
* @hide
*/
@NonNull
public List<BluetoothHapPresetInfo> getAllPresetInfo(@NonNull BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot get all preset info.");
return new ArrayList<>();
}
return mService.getAllPresetInfo(device);
}
/**
* Sets the preset name for a particular device
*
* <p>Note that the name length is restricted to 40 characters.
*
* <p>On success,
* {@link BluetoothHapClient.Callback#onPresetInfoChanged(BluetoothDevice, List, int)} with a
* new name will be called and reason code
* {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure,
* {@link BluetoothHapClient.Callback#onSetPresetNameFailed(BluetoothDevice, int)} will be
* called.
*
* @param device is the device for which we want to get the preset name
* @param presetIndex is an index of one of the available presets
* @param name is a new name for a preset, maximum length is 40 characters
* @hide
*/
public void setPresetName(@NonNull BluetoothDevice device, int presetIndex,
@NonNull String name) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot set preset name.");
return;
}
mService.setPresetName(device, presetIndex, name);
}
/**
* Sets the name for a hearing aid preset.
*
* <p>Note that the name length is restricted to 40 characters.
*
* <p>On success,
* {@link BluetoothHapClient.Callback#onPresetInfoChanged(BluetoothDevice, List, int)} with a
* new name will be called for each device within the group with reason code
* {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} On failure,
* {@link BluetoothHapClient.Callback#onSetPresetNameForGroupFailed(int, int)} will be invoked
*
* @param groupId is the device group identifier
* @param presetIndex is an index of one of the available presets
* @param name is a new name for a preset, maximum length is 40 characters
* @hide
*/
public void setPresetNameForGroup(int groupId, int presetIndex, @NonNull String name) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot set preset name for group.");
return;
}
mService.setPresetNameForGroup(groupId, presetIndex, name);
}
@Override
public boolean accessProfileEnabled() {
return false;
}
@Override
public boolean isAutoConnectable() {
return true;
}
@Override
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null || device == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null || device == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null || device == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
@Override
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.HAP_CLIENT;
}
@Override
public int getOrdinal() {
return ORDINAL;
}
@Override
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_hearing_aid;
}
@Override
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_hearing_aid_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_hearing_aid_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
@Override
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_hearing_aid;
}
/**
* Gets the name of this class
*
* @return the name of this class
*/
public String toString() {
return NAME;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HAP_CLIENT, mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up HAP Client proxy", t);
}
}
}
}

View File

@@ -0,0 +1,235 @@
/*
* Copyright (C) 2012 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
/**
* HeadsetProfile handles Bluetooth HFP and Headset profiles.
*/
public class HeadsetProfile implements LocalBluetoothProfile {
private static final String TAG = "HeadsetProfile";
private BluetoothHeadset mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
private final BluetoothAdapter mBluetoothAdapter;
static final ParcelUuid[] UUIDS = {
BluetoothUuid.HSP,
BluetoothUuid.HFP,
};
static final String NAME = "HEADSET";
// Order of this profile in device profiles list
private static final int ORDINAL = 0;
// These callbacks run on the main thread.
private final class HeadsetServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothHeadset) proxy;
// We just bound to the service, so refresh the UI for any connected HFP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "HeadsetProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(HeadsetProfile.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady=true;
mProfileManager.callServiceConnectedListeners();
}
public void onServiceDisconnected(int profile) {
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.HEADSET;
}
HeadsetProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mBluetoothAdapter.getProfileProxy(context, new HeadsetServiceListener(),
BluetoothProfile.HEADSET);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
return device == null
? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_PHONE_CALL)
: mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_PHONE_CALL);
}
public BluetoothDevice getActiveDevice() {
if (mBluetoothAdapter == null) {
return null;
}
final List<BluetoothDevice> activeDevices = mBluetoothAdapter
.getActiveDevices(BluetoothProfile.HEADSET);
return (activeDevices.size() > 0) ? activeDevices.get(0) : null;
}
public int getAudioState(BluetoothDevice device) {
if (mService == null) {
return BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
}
return mService.getAudioState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_headset;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_headset_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_headset_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_headset_hfp;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.HEADSET,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up HID proxy", t);
}
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright (C) 2023 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.settingslib.bluetooth;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Constant values used to configure hearing aid audio routing.
*
* {@link HearingAidAudioRoutingHelper}
*/
public final class HearingAidAudioRoutingConstants {
public static final int[] CALL_ROUTING_ATTRIBUTES = new int[] {
// Stands for STRATEGY_PHONE
AudioAttributes.USAGE_VOICE_COMMUNICATION,
};
public static final int[] MEDIA_ROUTING_ATTRIBUTES = new int[] {
// Stands for STRATEGY_MEDIA, including USAGE_GAME, USAGE_ASSISTANT,
// USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, USAGE_ASSISTANCE_SONIFICATION
AudioAttributes.USAGE_MEDIA,
// Stands for STRATEGY_ACCESSIBILITY
AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY,
// Stands for STRATEGY_DTMF
AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING,
};
public static final int[] RINGTONE_ROUTING_ATTRIBUTES = new int[] {
// Stands for STRATEGY_SONIFICATION, including USAGE_ALARM
AudioAttributes.USAGE_NOTIFICATION_RINGTONE
};
public static final int[] NOTIFICATION_ROUTING_ATTRIBUTES = new int[] {
// Stands for STRATEGY_SONIFICATION_RESPECTFUL, including USAGE_NOTIFICATION_EVENT
AudioAttributes.USAGE_NOTIFICATION,
};
@Retention(RetentionPolicy.SOURCE)
@IntDef({
RoutingValue.AUTO,
RoutingValue.HEARING_DEVICE,
RoutingValue.DEVICE_SPEAKER,
})
public @interface RoutingValue {
int AUTO = 0;
int HEARING_DEVICE = 1;
int DEVICE_SPEAKER = 2;
}
public static final AudioDeviceAttributes DEVICE_SPEAKER_OUT = new AudioDeviceAttributes(
AudioDeviceAttributes.ROLE_OUTPUT, AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, "");
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright (C) 2023 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.settingslib.bluetooth;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.audiopolicy.AudioProductStrategy;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* A helper class to configure the routing strategy for hearing aids.
*/
public class HearingAidAudioRoutingHelper {
private final AudioManager mAudioManager;
public HearingAidAudioRoutingHelper(Context context) {
mAudioManager = context.getSystemService(AudioManager.class);
}
/**
* Gets the list of {@link AudioProductStrategy} referred by the given list of usage values
* defined in {@link AudioAttributes}
*/
public List<AudioProductStrategy> getSupportedStrategies(int[] attributeSdkUsageList) {
final List<AudioAttributes> audioAttrList = new ArrayList<>(attributeSdkUsageList.length);
for (int attributeSdkUsage : attributeSdkUsageList) {
audioAttrList.add(new AudioAttributes.Builder().setUsage(attributeSdkUsage).build());
}
final List<AudioProductStrategy> allStrategies = getAudioProductStrategies();
final List<AudioProductStrategy> supportedStrategies = new ArrayList<>();
for (AudioProductStrategy strategy : allStrategies) {
for (AudioAttributes audioAttr : audioAttrList) {
if (strategy.supportsAudioAttributes(audioAttr)) {
supportedStrategies.add(strategy);
}
}
}
return supportedStrategies.stream().distinct().collect(Collectors.toList());
}
/**
* Sets the preferred device for the given strategies.
*
* @param supportedStrategies A list of {@link AudioProductStrategy} used to configure audio
* routing
* @param hearingDevice {@link AudioDeviceAttributes} of the device to be changed in audio
* routing
* @param routingValue one of value defined in
* {@link HearingAidAudioRoutingConstants.RoutingValue}, denotes routing
* destination.
* @return {code true} if the routing value successfully configure
*/
public boolean setPreferredDeviceRoutingStrategies(
List<AudioProductStrategy> supportedStrategies, AudioDeviceAttributes hearingDevice,
@HearingAidAudioRoutingConstants.RoutingValue int routingValue) {
boolean status;
switch (routingValue) {
case HearingAidAudioRoutingConstants.RoutingValue.AUTO:
status = removePreferredDeviceForStrategies(supportedStrategies);
return status;
case HearingAidAudioRoutingConstants.RoutingValue.HEARING_DEVICE:
status = removePreferredDeviceForStrategies(supportedStrategies);
status &= setPreferredDeviceForStrategies(supportedStrategies, hearingDevice);
return status;
case HearingAidAudioRoutingConstants.RoutingValue.DEVICE_SPEAKER:
status = removePreferredDeviceForStrategies(supportedStrategies);
status &= setPreferredDeviceForStrategies(supportedStrategies,
HearingAidAudioRoutingConstants.DEVICE_SPEAKER_OUT);
return status;
default:
throw new IllegalArgumentException("Unexpected routingValue: " + routingValue);
}
}
/**
* Gets the matched hearing device {@link AudioDeviceAttributes} for {@code device}.
*
* <p>Will also try to match the {@link CachedBluetoothDevice#getSubDevice()} of {@code device}
*
* @param device the {@link CachedBluetoothDevice} need to be hearing aid device
* @return the requested AudioDeviceAttributes or {@code null} if not match
*/
@Nullable
public AudioDeviceAttributes getMatchedHearingDeviceAttributes(CachedBluetoothDevice device) {
if (device == null || !device.isHearingAidDevice()) {
return null;
}
AudioDeviceInfo[] audioDevices = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
for (AudioDeviceInfo audioDevice : audioDevices) {
// ASHA for TYPE_HEARING_AID, HAP for TYPE_BLE_HEADSET
if (audioDevice.getType() == AudioDeviceInfo.TYPE_HEARING_AID
|| audioDevice.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) {
if (matchAddress(device, audioDevice)) {
return new AudioDeviceAttributes(audioDevice);
}
}
}
return null;
}
private boolean matchAddress(CachedBluetoothDevice device, AudioDeviceInfo audioDevice) {
final String audioDeviceAddress = audioDevice.getAddress();
final CachedBluetoothDevice subDevice = device.getSubDevice();
final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice();
return device.getAddress().equals(audioDeviceAddress)
|| (subDevice != null && subDevice.getAddress().equals(audioDeviceAddress))
|| (!memberDevices.isEmpty() && memberDevices.stream().anyMatch(
m -> m.getAddress().equals(audioDeviceAddress)));
}
private boolean setPreferredDeviceForStrategies(List<AudioProductStrategy> strategies,
AudioDeviceAttributes audioDevice) {
boolean status = true;
for (AudioProductStrategy strategy : strategies) {
status &= mAudioManager.setPreferredDeviceForStrategy(strategy, audioDevice);
}
return status;
}
private boolean removePreferredDeviceForStrategies(List<AudioProductStrategy> strategies) {
boolean status = true;
for (AudioProductStrategy strategy : strategies) {
if (mAudioManager.getPreferredDeviceForStrategy(strategy) != null) {
status &= mAudioManager.removePreferredDeviceForStrategy(strategy);
}
}
return status;
}
@VisibleForTesting
public List<AudioProductStrategy> getAudioProductStrategies() {
return AudioManager.getAudioProductStrategies();
}
}

View File

@@ -0,0 +1,390 @@
/*
* Copyright (C) 2018 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.settingslib.bluetooth;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.le.ScanFilter;
import android.content.ContentResolver;
import android.content.Context;
import android.media.AudioDeviceAttributes;
import android.media.audiopolicy.AudioProductStrategy;
import android.os.ParcelUuid;
import android.provider.Settings;
import android.util.FeatureFlagUtils;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* HearingAidDeviceManager manages the set of remote HearingAid(ASHA) Bluetooth devices.
*/
public class HearingAidDeviceManager {
private static final String TAG = "HearingAidDeviceManager";
private static final boolean DEBUG = BluetoothUtils.D;
private final ContentResolver mContentResolver;
private final Context mContext;
private final LocalBluetoothManager mBtManager;
private final List<CachedBluetoothDevice> mCachedDevices;
private final HearingAidAudioRoutingHelper mRoutingHelper;
HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager,
List<CachedBluetoothDevice> CachedDevices) {
mContext = context;
mContentResolver = context.getContentResolver();
mBtManager = localBtManager;
mCachedDevices = CachedDevices;
mRoutingHelper = new HearingAidAudioRoutingHelper(context);
}
@VisibleForTesting
HearingAidDeviceManager(Context context, LocalBluetoothManager localBtManager,
List<CachedBluetoothDevice> cachedDevices, HearingAidAudioRoutingHelper routingHelper) {
mContext = context;
mContentResolver = context.getContentResolver();
mBtManager = localBtManager;
mCachedDevices = cachedDevices;
mRoutingHelper = routingHelper;
}
void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice,
List<ScanFilter> leScanFilters) {
HearingAidInfo info = generateHearingAidInfo(newDevice);
if (info != null) {
newDevice.setHearingAidInfo(info);
} else if (leScanFilters != null && !newDevice.isHearingAidDevice()) {
// If the device is added with hearing aid scan filter during pairing, set an empty
// hearing aid info to indicate it's a hearing aid device. The info will be updated
// when corresponding profiles connected.
for (ScanFilter leScanFilter: leScanFilters) {
final ParcelUuid serviceUuid = leScanFilter.getServiceUuid();
final ParcelUuid serviceDataUuid = leScanFilter.getServiceDataUuid();
if (BluetoothUuid.HEARING_AID.equals(serviceUuid)
|| BluetoothUuid.HAS.equals(serviceUuid)
|| BluetoothUuid.HEARING_AID.equals(serviceDataUuid)
|| BluetoothUuid.HAS.equals(serviceDataUuid)) {
newDevice.setHearingAidInfo(new HearingAidInfo.Builder().build());
break;
}
}
}
}
boolean setSubDeviceIfNeeded(CachedBluetoothDevice newDevice) {
final long hiSyncId = newDevice.getHiSyncId();
if (isValidHiSyncId(hiSyncId)) {
final CachedBluetoothDevice hearingAidDevice = getCachedDevice(hiSyncId);
// Just add one of the hearing aids from a pair in the list that is shown in the UI.
// Once there is another device with the same hiSyncId, to add new device as sub
// device.
if (hearingAidDevice != null) {
hearingAidDevice.setSubDevice(newDevice);
return true;
}
}
return false;
}
private boolean isValidHiSyncId(long hiSyncId) {
return hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID;
}
private CachedBluetoothDevice getCachedDevice(long hiSyncId) {
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
if (cachedDevice.getHiSyncId() == hiSyncId) {
return cachedDevice;
}
}
return null;
}
// To collect all HearingAid devices and call #onHiSyncIdChanged to group device by HiSyncId
void updateHearingAidsDevices() {
final Set<Long> newSyncIdSet = new HashSet<>();
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
// Do nothing if HiSyncId has been assigned
if (isValidHiSyncId(cachedDevice.getHiSyncId())) {
continue;
}
HearingAidInfo info = generateHearingAidInfo(cachedDevice);
if (info != null) {
cachedDevice.setHearingAidInfo(info);
if (isValidHiSyncId(info.getHiSyncId())) {
newSyncIdSet.add(info.getHiSyncId());
}
}
}
for (Long syncId : newSyncIdSet) {
onHiSyncIdChanged(syncId);
}
}
// Group devices by hiSyncId
@VisibleForTesting
void onHiSyncIdChanged(long hiSyncId) {
int firstMatchedIndex = -1;
for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
if (cachedDevice.getHiSyncId() != hiSyncId) {
continue;
}
// The remote device supports CSIP, the other ear should be processed as a member
// device. Ignore hiSyncId grouping from ASHA here.
if (cachedDevice.getProfiles().stream().anyMatch(
profile -> profile instanceof CsipSetCoordinatorProfile)) {
continue;
}
if (firstMatchedIndex == -1) {
// Found the first one
firstMatchedIndex = i;
continue;
}
// Found the second one
int indexToRemoveFromUi;
CachedBluetoothDevice subDevice;
CachedBluetoothDevice mainDevice;
// Since the hiSyncIds have been updated for a connected pair of hearing aids,
// we remove the entry of one the hearing aids from the UI. Unless the
// hiSyncId get updated, the system does not know it is a hearing aid, so we add
// both the hearing aids as separate entries in the UI first, then remove one
// of them after the hiSyncId is populated. We will choose the device that
// is not connected to be removed.
if (cachedDevice.isConnected()) {
mainDevice = cachedDevice;
indexToRemoveFromUi = firstMatchedIndex;
subDevice = mCachedDevices.get(firstMatchedIndex);
} else {
mainDevice = mCachedDevices.get(firstMatchedIndex);
indexToRemoveFromUi = i;
subDevice = cachedDevice;
}
mainDevice.setSubDevice(subDevice);
mCachedDevices.remove(indexToRemoveFromUi);
log("onHiSyncIdChanged: removed from UI device =" + subDevice
+ ", with hiSyncId=" + hiSyncId);
mBtManager.getEventManager().dispatchDeviceRemoved(subDevice);
break;
}
}
// @return {@code true}, the event is processed inside the method. It is for updating
// hearing aid device on main-sub relationship when receiving connected or disconnected.
// @return {@code false}, it is not hearing aid device or to process it same as other profiles
boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice,
int state) {
switch (state) {
case BluetoothProfile.STATE_CONNECTED:
onHiSyncIdChanged(cachedDevice.getHiSyncId());
CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice);
if (mainDevice != null) {
if (mainDevice.isConnected()) {
// When main device exists and in connected state, receiving sub device
// connection. To refresh main device UI
mainDevice.refresh();
} else {
// When both Hearing Aid devices are disconnected, receiving sub device
// connection. To switch content and dispatch to notify UI change
mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice);
mainDevice.switchSubDeviceContent();
mainDevice.refresh();
// It is necessary to do remove and add for updating the mapping on
// preference and device
mBtManager.getEventManager().dispatchDeviceAdded(mainDevice);
}
return true;
}
break;
case BluetoothProfile.STATE_DISCONNECTED:
mainDevice = findMainDevice(cachedDevice);
if (cachedDevice.getUnpairing()) {
return true;
}
if (mainDevice != null) {
// When main device exists, receiving sub device disconnection
// To update main device UI
mainDevice.refresh();
return true;
}
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null && subDevice.isConnected()) {
// Main device is disconnected and sub device is connected
// To copy data from sub device to main device
mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice);
cachedDevice.switchSubDeviceContent();
cachedDevice.refresh();
// It is necessary to do remove and add for updating the mapping on
// preference and device
mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice);
return true;
}
break;
}
return false;
}
void onActiveDeviceChanged(CachedBluetoothDevice device) {
if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) {
if (device.isActiveDevice(BluetoothProfile.HEARING_AID) || device.isActiveDevice(
BluetoothProfile.LE_AUDIO)) {
setAudioRoutingConfig(device);
} else {
clearAudioRoutingConfig();
}
}
}
private void setAudioRoutingConfig(CachedBluetoothDevice device) {
AudioDeviceAttributes hearingDeviceAttributes =
mRoutingHelper.getMatchedHearingDeviceAttributes(device);
if (hearingDeviceAttributes == null) {
Log.w(TAG, "Can not find expected AudioDeviceAttributes for hearing device: "
+ device.getDevice().getAnonymizedAddress());
return;
}
final int callRoutingValue = Settings.Secure.getInt(mContentResolver,
Settings.Secure.HEARING_AID_CALL_ROUTING,
HearingAidAudioRoutingConstants.RoutingValue.AUTO);
final int mediaRoutingValue = Settings.Secure.getInt(mContentResolver,
Settings.Secure.HEARING_AID_MEDIA_ROUTING,
HearingAidAudioRoutingConstants.RoutingValue.AUTO);
final int ringtoneRoutingValue = Settings.Secure.getInt(mContentResolver,
Settings.Secure.HEARING_AID_RINGTONE_ROUTING,
HearingAidAudioRoutingConstants.RoutingValue.AUTO);
final int systemSoundsRoutingValue = Settings.Secure.getInt(mContentResolver,
Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING,
HearingAidAudioRoutingConstants.RoutingValue.AUTO);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES,
hearingDeviceAttributes, callRoutingValue);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES,
hearingDeviceAttributes, mediaRoutingValue);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTES,
hearingDeviceAttributes, ringtoneRoutingValue);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.NOTIFICATION_ROUTING_ATTRIBUTES,
hearingDeviceAttributes, systemSoundsRoutingValue);
}
private void clearAudioRoutingConfig() {
// Don't need to pass hearingDevice when we want to reset it (set to AUTO).
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.CALL_ROUTING_ATTRIBUTES,
/* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.MEDIA_ROUTING_ATTRIBUTES,
/* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.RINGTONE_ROUTING_ATTRIBUTES,
/* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
setPreferredDeviceRoutingStrategies(
HearingAidAudioRoutingConstants.NOTIFICATION_ROUTING_ATTRIBUTES,
/* hearingDevice = */ null, HearingAidAudioRoutingConstants.RoutingValue.AUTO);
}
private void setPreferredDeviceRoutingStrategies(int[] attributeSdkUsageList,
AudioDeviceAttributes hearingDevice,
@HearingAidAudioRoutingConstants.RoutingValue int routingValue) {
final List<AudioProductStrategy> supportedStrategies =
mRoutingHelper.getSupportedStrategies(attributeSdkUsageList);
final boolean status = mRoutingHelper.setPreferredDeviceRoutingStrategies(
supportedStrategies, hearingDevice, routingValue);
if (!status) {
Log.w(TAG, "routingStrategies: " + supportedStrategies.toString() + "routingValue: "
+ routingValue + " fail to configure AudioProductStrategy");
}
}
CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) {
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
if (isValidHiSyncId(cachedDevice.getHiSyncId())) {
CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
if (subDevice != null && subDevice.equals(device)) {
return cachedDevice;
}
}
}
return null;
}
private HearingAidInfo generateHearingAidInfo(CachedBluetoothDevice cachedDevice) {
final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
final HearingAidProfile asha = profileManager.getHearingAidProfile();
if (asha == null) {
Log.w(TAG, "HearingAidProfile is not supported on this device");
} else {
long hiSyncId = asha.getHiSyncId(cachedDevice.getDevice());
if (isValidHiSyncId(hiSyncId)) {
final HearingAidInfo info = new HearingAidInfo.Builder()
.setAshaDeviceSide(asha.getDeviceSide(cachedDevice.getDevice()))
.setAshaDeviceMode(asha.getDeviceMode(cachedDevice.getDevice()))
.setHiSyncId(hiSyncId)
.build();
if (DEBUG) {
Log.d(TAG, "generateHearingAidInfo, " + cachedDevice + ", info=" + info);
}
return info;
}
}
final HapClientProfile hapClientProfile = profileManager.getHapClientProfile();
final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile();
if (hapClientProfile == null || leAudioProfile == null) {
Log.w(TAG, "HapClientProfile or LeAudioProfile is not supported on this device");
} else if (cachedDevice.getProfiles().stream().anyMatch(
p -> p instanceof HapClientProfile)) {
int audioLocation = leAudioProfile.getAudioLocation(cachedDevice.getDevice());
int hearingAidType = hapClientProfile.getHearingAidType(cachedDevice.getDevice());
if (audioLocation != BluetoothLeAudio.AUDIO_LOCATION_INVALID
&& hearingAidType != HapClientProfile.HearingAidType.TYPE_INVALID) {
final HearingAidInfo info = new HearingAidInfo.Builder()
.setLeAudioLocation(audioLocation)
.setHapDeviceType(hearingAidType)
.build();
if (DEBUG) {
Log.d(TAG, "generateHearingAidInfo, " + cachedDevice + ", info=" + info);
}
return info;
}
}
return null;
}
private void log(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
}
}
}

View File

@@ -0,0 +1,263 @@
/*
* 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.settingslib.bluetooth;
import android.annotation.IntDef;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothLeAudio;
import android.util.SparseIntArray;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
/** Hearing aids information and constants that shared within hearing aids related profiles */
public class HearingAidInfo {
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DeviceSide.SIDE_INVALID,
DeviceSide.SIDE_LEFT,
DeviceSide.SIDE_RIGHT,
DeviceSide.SIDE_LEFT_AND_RIGHT,
})
/** Side definition for hearing aids. */
public @interface DeviceSide {
int SIDE_INVALID = -1;
int SIDE_LEFT = 0;
int SIDE_RIGHT = 1;
int SIDE_LEFT_AND_RIGHT = 2;
}
@Retention(java.lang.annotation.RetentionPolicy.SOURCE)
@IntDef({
DeviceMode.MODE_INVALID,
DeviceMode.MODE_MONAURAL,
DeviceMode.MODE_BINAURAL,
DeviceMode.MODE_BANDED,
})
/** Mode definition for hearing aids. */
public @interface DeviceMode {
int MODE_INVALID = -1;
int MODE_MONAURAL = 0;
int MODE_BINAURAL = 1;
int MODE_BANDED = 2;
}
private final int mSide;
private final int mMode;
private final long mHiSyncId;
private HearingAidInfo(int side, int mode, long hiSyncId) {
mSide = side;
mMode = mode;
mHiSyncId = hiSyncId;
}
@DeviceSide
public int getSide() {
return mSide;
}
@DeviceMode
public int getMode() {
return mMode;
}
public long getHiSyncId() {
return mHiSyncId;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof HearingAidInfo)) {
return false;
}
HearingAidInfo that = (HearingAidInfo) o;
return mSide == that.mSide && mMode == that.mMode && mHiSyncId == that.mHiSyncId;
}
@Override
public int hashCode() {
return Objects.hash(mSide, mMode, mHiSyncId);
}
@Override
public String toString() {
return "HearingAidInfo{"
+ "mSide=" + mSide
+ ", mMode=" + mMode
+ ", mHiSyncId=" + mHiSyncId
+ '}';
}
@DeviceSide
private static int convertAshaDeviceSideToInternalSide(int ashaDeviceSide) {
return ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING.get(
ashaDeviceSide, DeviceSide.SIDE_INVALID);
}
@DeviceMode
private static int convertAshaDeviceModeToInternalMode(int ashaDeviceMode) {
return ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING.get(
ashaDeviceMode, DeviceMode.MODE_INVALID);
}
@DeviceSide
private static int convertLeAudioLocationToInternalSide(int leAudioLocation) {
boolean isLeft = (leAudioLocation & LE_AUDIO_LOCATION_LEFT) != 0;
boolean isRight = (leAudioLocation & LE_AUDIO_LOCATION_RIGHT) != 0;
if (isLeft && isRight) {
return DeviceSide.SIDE_LEFT_AND_RIGHT;
} else if (isLeft) {
return DeviceSide.SIDE_LEFT;
} else if (isRight) {
return DeviceSide.SIDE_RIGHT;
}
return DeviceSide.SIDE_INVALID;
}
@DeviceMode
private static int convertHapDeviceTypeToInternalMode(int hapDeviceType) {
return HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.get(hapDeviceType, DeviceMode.MODE_INVALID);
}
/** Builder class for constructing {@link HearingAidInfo} objects. */
public static final class Builder {
private int mSide = DeviceSide.SIDE_INVALID;
private int mMode = DeviceMode.MODE_INVALID;
private long mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID;
/**
* Configure the hearing device mode.
* @param ashaDeviceMode one of the hearing aid device modes defined in HearingAidProfile
* {@link HearingAidProfile.DeviceMode}
*/
public Builder setAshaDeviceMode(int ashaDeviceMode) {
mMode = convertAshaDeviceModeToInternalMode(ashaDeviceMode);
return this;
}
/**
* Configure the hearing device mode.
* @param hapDeviceType one of the hearing aid device types defined in HapClientProfile
* {@link HapClientProfile.HearingAidType}
*/
public Builder setHapDeviceType(int hapDeviceType) {
mMode = convertHapDeviceTypeToInternalMode(hapDeviceType);
return this;
}
/**
* Configure the hearing device side.
* @param ashaDeviceSide one of the hearing aid device sides defined in HearingAidProfile
* {@link HearingAidProfile.DeviceSide}
*/
public Builder setAshaDeviceSide(int ashaDeviceSide) {
mSide = convertAshaDeviceSideToInternalSide(ashaDeviceSide);
return this;
}
/**
* Configure the hearing device side.
* @param leAudioLocation one of the audio location defined in BluetoothLeAudio
* {@link BluetoothLeAudio.AudioLocation}
*/
public Builder setLeAudioLocation(int leAudioLocation) {
mSide = convertLeAudioLocationToInternalSide(leAudioLocation);
return this;
}
/**
* Configure the hearing aid hiSyncId.
* @param hiSyncId the ASHA hearing aid id
*/
public Builder setHiSyncId(long hiSyncId) {
mHiSyncId = hiSyncId;
return this;
}
/** Build the configured {@link HearingAidInfo} */
public HearingAidInfo build() {
return new HearingAidInfo(mSide, mMode, mHiSyncId);
}
}
private static final int LE_AUDIO_LOCATION_LEFT =
BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_BACK_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_OF_CENTER
| BluetoothLeAudio.AUDIO_LOCATION_SIDE_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_LEFT
| BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT_WIDE
| BluetoothLeAudio.AUDIO_LOCATION_LEFT_SURROUND;
private static final int LE_AUDIO_LOCATION_RIGHT =
BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_BACK_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_OF_CENTER
| BluetoothLeAudio.AUDIO_LOCATION_SIDE_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_TOP_FRONT_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_TOP_BACK_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_TOP_SIDE_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_BOTTOM_FRONT_RIGHT
| BluetoothLeAudio.AUDIO_LOCATION_FRONT_RIGHT_WIDE
| BluetoothLeAudio.AUDIO_LOCATION_RIGHT_SURROUND;
private static final SparseIntArray ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING;
private static final SparseIntArray ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING;
private static final SparseIntArray HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING;
static {
ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING = new SparseIntArray();
ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING.put(
HearingAidProfile.DeviceSide.SIDE_INVALID, DeviceSide.SIDE_INVALID);
ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING.put(
HearingAidProfile.DeviceSide.SIDE_LEFT, DeviceSide.SIDE_LEFT);
ASHA_DEVICE_SIDE_TO_INTERNAL_SIDE_MAPPING.put(
HearingAidProfile.DeviceSide.SIDE_RIGHT, DeviceSide.SIDE_RIGHT);
ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING = new SparseIntArray();
ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING.put(
HearingAidProfile.DeviceMode.MODE_INVALID, DeviceMode.MODE_INVALID);
ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING.put(
HearingAidProfile.DeviceMode.MODE_MONAURAL, DeviceMode.MODE_MONAURAL);
ASHA_DEVICE_MODE_TO_INTERNAL_MODE_MAPPING.put(
HearingAidProfile.DeviceMode.MODE_BINAURAL, DeviceMode.MODE_BINAURAL);
HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING = new SparseIntArray();
HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
HapClientProfile.HearingAidType.TYPE_INVALID, DeviceMode.MODE_INVALID);
HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
HapClientProfile.HearingAidType.TYPE_BINAURAL, DeviceMode.MODE_BINAURAL);
HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
HapClientProfile.HearingAidType.TYPE_MONAURAL, DeviceMode.MODE_MONAURAL);
HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
HapClientProfile.HearingAidType.TYPE_BANDED, DeviceMode.MODE_BANDED);
HAP_DEVICE_TYPE_TO_INTERNAL_MODE_MAPPING.put(
HapClientProfile.HearingAidType.TYPE_RFU, DeviceMode.MODE_INVALID);
}
}

View File

@@ -0,0 +1,350 @@
/*
* Copyright (C) 2018 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_AUDIO;
import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import com.android.settingslib.R;
import com.android.settingslib.Utils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
public class HearingAidProfile implements LocalBluetoothProfile {
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DeviceSide.SIDE_INVALID,
DeviceSide.SIDE_LEFT,
DeviceSide.SIDE_RIGHT
})
/** Side definition for hearing aids. See {@link BluetoothHearingAid}. */
public @interface DeviceSide {
int SIDE_INVALID = -1;
int SIDE_LEFT = 0;
int SIDE_RIGHT = 1;
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DeviceMode.MODE_INVALID,
DeviceMode.MODE_MONAURAL,
DeviceMode.MODE_BINAURAL
})
/** Mode definition for hearing aids. See {@link BluetoothHearingAid}. */
public @interface DeviceMode {
int MODE_INVALID = -1;
int MODE_MONAURAL = 0;
int MODE_BINAURAL = 1;
}
private static final String TAG = "HearingAidProfile";
private static boolean V = true;
private Context mContext;
private BluetoothHearingAid mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
static final String NAME = "HearingAid";
private final LocalBluetoothProfileManager mProfileManager;
private final BluetoothAdapter mBluetoothAdapter;
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
// These callbacks run on the main thread.
private final class HearingAidServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothHearingAid) proxy;
// We just bound to the service, so refresh the UI for any connected HearingAid devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
if (V) {
Log.d(TAG, "HearingAidProfile found new device: " + nextDevice);
}
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(HearingAidProfile.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
// Check current list of CachedDevices to see if any are hearing aid devices.
mDeviceManager.updateHearingAidsDevices();
mIsProfileReady = true;
mProfileManager.callServiceConnectedListeners();
}
public void onServiceDisconnected(int profile) {
mIsProfileReady = false;
mProfileManager.callServiceDisconnectedListeners();
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.HEARING_AID;
}
HearingAidProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mContext = context;
mDeviceManager = deviceManager;
mProfileManager = profileManager;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mBluetoothAdapter.getProfileProxy(context,
new HearingAidServiceListener(), BluetoothProfile.HEARING_AID);
}
public boolean accessProfileEnabled() {
return false;
}
public boolean isAutoConnectable() {
return true;
}
/**
* Get Hearing Aid devices matching connection states{
* @code BluetoothProfile.STATE_CONNECTED,
* @code BluetoothProfile.STATE_CONNECTING,
* @code BluetoothProfile.STATE_DISCONNECTING}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectedDevices() {
return getDevicesByStates(new int[] {
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
/**
* Get Hearing Aid devices matching connection states{
* @code BluetoothProfile.STATE_DISCONNECTED,
* @code BluetoothProfile.STATE_CONNECTED,
* @code BluetoothProfile.STATE_CONNECTING,
* @code BluetoothProfile.STATE_DISCONNECTING}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectableDevices() {
return getDevicesByStates(new int[] {
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
private List<BluetoothDevice> getDevicesByStates(int[] states) {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(states);
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
int profiles = Utils.isAudioModeOngoingCall(mContext)
? ACTIVE_DEVICE_PHONE_CALL
: ACTIVE_DEVICE_AUDIO;
return device == null
? mBluetoothAdapter.removeActiveDevice(profiles)
: mBluetoothAdapter.setActiveDevice(device, profiles);
}
public List<BluetoothDevice> getActiveDevices() {
if (mBluetoothAdapter == null) {
return new ArrayList<>();
}
return mBluetoothAdapter.getActiveDevices(BluetoothProfile.HEARING_AID);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null || device == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null || device == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null || device == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
/**
* Tells remote device to set an absolute volume.
*
* @param volume Absolute volume to be set on remote
*/
public void setVolume(int volume) {
if (mService == null) {
return;
}
mService.setVolume(volume);
}
/**
* Gets the HiSyncId (unique hearing aid device identifier) of the device.
*
* @param device Bluetooth device
* @return the HiSyncId of the device
*/
public long getHiSyncId(BluetoothDevice device) {
if (mService == null || device == null) {
return BluetoothHearingAid.HI_SYNC_ID_INVALID;
}
return mService.getHiSyncId(device);
}
/**
* Gets the side of the device.
*
* @param device Bluetooth device.
* @return side of the device. See {@link DeviceSide}.
*/
@DeviceSide
public int getDeviceSide(@NonNull BluetoothDevice device) {
final int defaultValue = DeviceSide.SIDE_INVALID;
if (mService == null) {
Log.w(TAG, "Proxy not attached to HearingAidService");
return defaultValue;
}
return mService.getDeviceSide(device);
}
/**
* Gets the mode of the device.
*
* @param device Bluetooth device
* @return mode of the device. See {@link DeviceMode}.
*/
@DeviceMode
public int getDeviceMode(@NonNull BluetoothDevice device) {
final int defaultValue = DeviceMode.MODE_INVALID;
if (mService == null) {
Log.w(TAG, "Proxy not attached to HearingAidService");
return defaultValue;
}
return mService.getDeviceMode(device);
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_hearing_aid;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_hearing_aid_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_hearing_aid_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_hearing_aid;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.HEARING_AID,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up Hearing Aid proxy", t);
}
}
}
}

View File

@@ -0,0 +1,360 @@
/*
* 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.settingslib.bluetooth;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import java.util.stream.Collectors;
/** Utils class to report hearing aid metrics to statsd */
public final class HearingAidStatsLogUtils {
private static final String TAG = "HearingAidStatsLogUtils";
private static final boolean DEBUG = true;
private static final String ACCESSIBILITY_PREFERENCE = "accessibility_prefs";
private static final String BT_HEARING_AIDS_PAIRED_HISTORY = "bt_hearing_aids_paired_history";
private static final String BT_HEARING_AIDS_CONNECTED_HISTORY =
"bt_hearing_aids_connected_history";
private static final String BT_HEARING_DEVICES_PAIRED_HISTORY =
"bt_hearing_devices_paired_history";
private static final String BT_HEARING_DEVICES_CONNECTED_HISTORY =
"bt_hearing_devices_connected_history";
private static final String BT_HEARING_USER_CATEGORY = "bt_hearing_user_category";
private static final String HISTORY_RECORD_DELIMITER = ",";
static final String CATEGORY_HEARING_AIDS = "A11yHearingAidsUser";
static final String CATEGORY_NEW_HEARING_AIDS = "A11yNewHearingAidsUser";
static final String CATEGORY_HEARING_DEVICES = "A11yHearingDevicesUser";
static final String CATEGORY_NEW_HEARING_DEVICES = "A11yNewHearingDevicesUser";
static final int PAIRED_HISTORY_EXPIRED_DAY = 30;
static final int CONNECTED_HISTORY_EXPIRED_DAY = 7;
private static final int VALID_PAIRED_EVENT_COUNT = 1;
private static final int VALID_CONNECTED_EVENT_COUNT = 7;
/**
* Type of different Bluetooth device events history related to hearing.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({
HistoryType.TYPE_UNKNOWN,
HistoryType.TYPE_HEARING_AIDS_PAIRED,
HistoryType.TYPE_HEARING_AIDS_CONNECTED,
HistoryType.TYPE_HEARING_DEVICES_PAIRED,
HistoryType.TYPE_HEARING_DEVICES_CONNECTED})
public @interface HistoryType {
int TYPE_UNKNOWN = -1;
int TYPE_HEARING_AIDS_PAIRED = 0;
int TYPE_HEARING_AIDS_CONNECTED = 1;
int TYPE_HEARING_DEVICES_PAIRED = 2;
int TYPE_HEARING_DEVICES_CONNECTED = 3;
}
private static final HashMap<String, Integer> sDeviceAddressToBondEntryMap = new HashMap<>();
private static final Set<String> sJustBondedDeviceAddressSet = new HashSet<>();
/**
* Sets the mapping from hearing aid device to the bond entry where this device starts it's
* bonding(connecting) process.
*
* @param bondEntry The entry page id where the bonding process starts
* @param device The bonding(connecting) hearing aid device
*/
public static void setBondEntryForDevice(int bondEntry, CachedBluetoothDevice device) {
sDeviceAddressToBondEntryMap.put(device.getAddress(), bondEntry);
}
/**
* Logs hearing aid device information to statsd, including device mode, device side, and entry
* page id where the binding(connecting) process starts.
*
* Only logs the info once after hearing aid is bonded(connected). Clears the map entry of this
* device when logging is completed.
*
* @param device The bonded(connected) hearing aid device
*/
public static void logHearingAidInfo(CachedBluetoothDevice device) {
final String deviceAddress = device.getAddress();
if (sDeviceAddressToBondEntryMap.containsKey(deviceAddress)) {
final int bondEntry = sDeviceAddressToBondEntryMap.getOrDefault(deviceAddress, -1);
final int deviceMode = device.getDeviceMode();
final int deviceSide = device.getDeviceSide();
FrameworkStatsLog.write(FrameworkStatsLog.HEARING_AID_INFO_REPORTED, deviceMode,
deviceSide, bondEntry);
sDeviceAddressToBondEntryMap.remove(deviceAddress);
} else {
Log.w(TAG, "The device address was not found. Hearing aid device info is not logged.");
}
}
@VisibleForTesting
static HashMap<String, Integer> getDeviceAddressToBondEntryMap() {
return sDeviceAddressToBondEntryMap;
}
/**
* Updates corresponding history if we found the device is a hearing device after profile state
* changed.
*
* @param context the request context
* @param cachedDevice the remote device
* @param profile the profile that has a state changed
* @param profileState the new profile state
*/
public static void updateHistoryIfNeeded(Context context, CachedBluetoothDevice cachedDevice,
LocalBluetoothProfile profile, int profileState) {
if (isJustBonded(cachedDevice.getAddress())) {
// Saves bonded timestamp as the source for judging whether to display
// the survey
if (cachedDevice.getProfiles().stream().anyMatch(
p -> (p instanceof HearingAidProfile || p instanceof HapClientProfile))) {
HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_AIDS_PAIRED);
} else if (cachedDevice.getProfiles().stream().anyMatch(
p -> (p instanceof A2dpSinkProfile || p instanceof HeadsetProfile))) {
HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_PAIRED);
}
removeFromJustBonded(cachedDevice.getAddress());
}
// Saves connected timestamp as the source for judging whether to display
// the survey
if (profileState == BluetoothProfile.STATE_CONNECTED) {
if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) {
HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_AIDS_CONNECTED);
} else if (profile instanceof A2dpSinkProfile || profile instanceof HeadsetProfile) {
HearingAidStatsLogUtils.addCurrentTimeToHistory(context,
HearingAidStatsLogUtils.HistoryType.TYPE_HEARING_DEVICES_CONNECTED);
}
}
}
/**
* Returns the user category if the user is already categorized. Otherwise, checks the
* history and sees if the user is categorized as one of {@link #CATEGORY_HEARING_AIDS},
* {@link #CATEGORY_NEW_HEARING_AIDS}, {@link #CATEGORY_HEARING_DEVICES}, and
* {@link #CATEGORY_NEW_HEARING_DEVICES}.
*
* @param context the request context
* @return the category which user belongs to
*/
public static synchronized String getUserCategory(Context context) {
String userCategory = getSharedPreferences(context).getString(BT_HEARING_USER_CATEGORY, "");
if (!userCategory.isEmpty()) {
return userCategory;
}
LinkedList<Long> hearingAidsConnectedHistory = getHistory(context,
HistoryType.TYPE_HEARING_AIDS_CONNECTED);
if (hearingAidsConnectedHistory != null
&& hearingAidsConnectedHistory.size() >= VALID_CONNECTED_EVENT_COUNT) {
LinkedList<Long> hearingAidsPairedHistory = getHistory(context,
HistoryType.TYPE_HEARING_AIDS_PAIRED);
// Since paired history will be cleared after 30 days. If there's any record within 30
// days, the user will be categorized as CATEGORY_NEW_HEARING_AIDS. Otherwise, the user
// will be categorized as CATEGORY_HEARING_AIDS.
if (hearingAidsPairedHistory != null
&& hearingAidsPairedHistory.size() >= VALID_PAIRED_EVENT_COUNT) {
userCategory = CATEGORY_NEW_HEARING_AIDS;
} else {
userCategory = CATEGORY_HEARING_AIDS;
}
}
LinkedList<Long> hearingDevicesConnectedHistory = getHistory(context,
HistoryType.TYPE_HEARING_DEVICES_CONNECTED);
if (hearingDevicesConnectedHistory != null
&& hearingDevicesConnectedHistory.size() >= VALID_CONNECTED_EVENT_COUNT) {
LinkedList<Long> hearingDevicesPairedHistory = getHistory(context,
HistoryType.TYPE_HEARING_DEVICES_PAIRED);
// Since paired history will be cleared after 30 days. If there's any record within 30
// days, the user will be categorized as CATEGORY_NEW_HEARING_DEVICES. Otherwise, the
// user will be categorized as CATEGORY_HEARING_DEVICES.
if (hearingDevicesPairedHistory != null
&& hearingDevicesPairedHistory.size() >= VALID_PAIRED_EVENT_COUNT) {
userCategory = CATEGORY_NEW_HEARING_DEVICES;
} else {
userCategory = CATEGORY_HEARING_DEVICES;
}
}
return userCategory;
}
/**
* Maintains a temporarily list of just bonded device address. After the device profiles are
* connected, {@link HearingAidStatsLogUtils#removeFromJustBonded} will be called to remove the
* address.
* @param address the device address
*/
public static void addToJustBonded(String address) {
sJustBondedDeviceAddressSet.add(address);
}
/**
* Removes the device address from the just bonded list.
* @param address the device address
*/
private static void removeFromJustBonded(String address) {
sJustBondedDeviceAddressSet.remove(address);
}
/**
* Checks whether the device address is in the just bonded list.
* @param address the device address
* @return true if the device address is in the just bonded list
*/
private static boolean isJustBonded(String address) {
return sJustBondedDeviceAddressSet.contains(address);
}
/**
* Adds current timestamp into BT hearing devices related history.
* @param context the request context
* @param type the type of history to store the data. See {@link HistoryType}.
*/
public static void addCurrentTimeToHistory(Context context, @HistoryType int type) {
addToHistory(context, type, System.currentTimeMillis());
}
static synchronized void addToHistory(Context context, @HistoryType int type,
long timestamp) {
LinkedList<Long> history = getHistory(context, type);
if (history == null) {
if (DEBUG) {
Log.w(TAG, "Couldn't find shared preference name matched type=" + type);
}
return;
}
if (history.peekLast() != null && isSameDay(timestamp, history.peekLast())) {
if (DEBUG) {
Log.w(TAG, "Skip this record, it's same day record");
}
return;
}
history.add(timestamp);
SharedPreferences.Editor editor = getSharedPreferences(context).edit();
editor.putString(HISTORY_TYPE_TO_SP_NAME_MAPPING.get(type),
convertToHistoryString(history)).apply();
}
@Nullable
static synchronized LinkedList<Long> getHistory(Context context, @HistoryType int type) {
String spName = HISTORY_TYPE_TO_SP_NAME_MAPPING.get(type);
if (BT_HEARING_AIDS_PAIRED_HISTORY.equals(spName)
|| BT_HEARING_DEVICES_PAIRED_HISTORY.equals(spName)) {
LinkedList<Long> history = convertToHistoryList(
getSharedPreferences(context).getString(spName, ""));
removeRecordsBeforeDay(history, PAIRED_HISTORY_EXPIRED_DAY);
return history;
} else if (BT_HEARING_AIDS_CONNECTED_HISTORY.equals(spName)
|| BT_HEARING_DEVICES_CONNECTED_HISTORY.equals(spName)) {
LinkedList<Long> history = convertToHistoryList(
getSharedPreferences(context).getString(spName, ""));
removeRecordsBeforeDay(history, CONNECTED_HISTORY_EXPIRED_DAY);
return history;
}
return null;
}
private static void removeRecordsBeforeDay(LinkedList<Long> history, int day) {
if (history == null || history.isEmpty()) {
return;
}
long currentTime = System.currentTimeMillis();
while (history.peekFirst() != null
&& dayDifference(currentTime, history.peekFirst()) >= day) {
history.poll();
}
}
private static String convertToHistoryString(LinkedList<Long> history) {
return history.stream().map(Object::toString).collect(
Collectors.joining(HISTORY_RECORD_DELIMITER));
}
private static LinkedList<Long> convertToHistoryList(String string) {
if (string == null || string.isEmpty()) {
return new LinkedList<>();
}
LinkedList<Long> ll = new LinkedList<>();
String[] elements = string.split(HISTORY_RECORD_DELIMITER);
for (String e: elements) {
if (e.isEmpty()) continue;
ll.offer(Long.parseLong(e));
}
return ll;
}
/**
* Check if two timestamps are in the same date according to current timezone. This function
* doesn't consider the original timezone when the timestamp is saved.
*
* @param t1 the first epoch timestamp
* @param t2 the second epoch timestamp
* @return {@code true} if two timestamps are on the same day
*/
private static boolean isSameDay(long t1, long t2) {
return dayDifference(t1, t2) == 0;
}
private static long dayDifference(long t1, long t2) {
ZoneId zoneId = ZoneId.systemDefault();
LocalDate date1 = Instant.ofEpochMilli(t1).atZone(zoneId).toLocalDate();
LocalDate date2 = Instant.ofEpochMilli(t2).atZone(zoneId).toLocalDate();
return Math.abs(ChronoUnit.DAYS.between(date1, date2));
}
private static SharedPreferences getSharedPreferences(Context context) {
return context.getSharedPreferences(ACCESSIBILITY_PREFERENCE, Context.MODE_PRIVATE);
}
private static final HashMap<Integer, String> HISTORY_TYPE_TO_SP_NAME_MAPPING;
static {
HISTORY_TYPE_TO_SP_NAME_MAPPING = new HashMap<>();
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
HistoryType.TYPE_HEARING_AIDS_PAIRED, BT_HEARING_AIDS_PAIRED_HISTORY);
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
HistoryType.TYPE_HEARING_AIDS_CONNECTED, BT_HEARING_AIDS_CONNECTED_HISTORY);
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
HistoryType.TYPE_HEARING_DEVICES_PAIRED, BT_HEARING_DEVICES_PAIRED_HISTORY);
HISTORY_TYPE_TO_SP_NAME_MAPPING.put(
HistoryType.TYPE_HEARING_DEVICES_CONNECTED, BT_HEARING_DEVICES_CONNECTED_HISTORY);
}
private HearingAidStatsLogUtils() {}
}

View File

@@ -0,0 +1,215 @@
/*
* Copyright (C) 2016 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
/**
* Handles the Handsfree HF role.
*/
final class HfpClientProfile implements LocalBluetoothProfile {
private static final String TAG = "HfpClientProfile";
private BluetoothHeadsetClient mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
static final ParcelUuid[] SRC_UUIDS = {
BluetoothUuid.HSP_AG,
BluetoothUuid.HFP_AG,
};
static final String NAME = "HEADSET_CLIENT";
private final LocalBluetoothProfileManager mProfileManager;
// Order of this profile in device profiles list
private static final int ORDINAL = 0;
// These callbacks run on the main thread.
private final class HfpClientServiceListener
implements BluetoothProfile.ServiceListener {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothHeadsetClient) proxy;
// We just bound to the service, so refresh the UI for any connected HFP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "HfpClient profile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(
HfpClientProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady=true;
}
@Override
public void onServiceDisconnected(int profile) {
mIsProfileReady=false;
}
}
@Override
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.HEADSET_CLIENT;
}
HfpClientProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context,
new HfpClientServiceListener(), BluetoothProfile.HEADSET_CLIENT);
}
@Override
public boolean accessProfileEnabled() {
return true;
}
@Override
public boolean isAutoConnectable() {
return true;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
@Override
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
@Override
public String toString() {
return NAME;
}
@Override
public int getOrdinal() {
return ORDINAL;
}
@Override
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_headset;
}
@Override
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_headset_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_headset_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
@Override
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_headset_hfp;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(
BluetoothProfile.HEADSET_CLIENT, mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up HfpClient proxy", t);
}
}
}
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHidDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import com.android.settingslib.R;
import java.util.List;
/**
* HidDeviceProfile handles Bluetooth HID Device role
*/
public class HidDeviceProfile implements LocalBluetoothProfile {
private static final String TAG = "HidDeviceProfile";
// Order of this profile in device profiles list
private static final int ORDINAL = 18;
// HID Device Profile is always preferred.
private static final int PREFERRED_VALUE = -1;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
static final String NAME = "HID DEVICE";
private BluetoothHidDevice mService;
private boolean mIsProfileReady;
HidDeviceProfile(Context context,CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context,
new HidDeviceServiceListener(), BluetoothProfile.HID_DEVICE);
}
// These callbacks run on the main thread.
private final class HidDeviceServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothHidDevice) proxy;
// We just bound to the service, so refresh the UI for any connected HID devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
for (BluetoothDevice nextDevice : deviceList) {
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "HidProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
Log.d(TAG, "Connection status changed: " + device);
device.onProfileStateChanged(HidDeviceProfile.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady = true;
}
public void onServiceDisconnected(int profile) {
mIsProfileReady = false;
}
}
@Override
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.HID_DEVICE;
}
@Override
public boolean accessProfileEnabled() {
return true;
}
@Override
public boolean isAutoConnectable() {
return false;
}
@Override
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
return getConnectionStatus(device) != BluetoothProfile.STATE_DISCONNECTED;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
return PREFERRED_VALUE;
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
// if set preferred to false, then disconnect to the current device
if (!enabled) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
@Override
public String toString() {
return NAME;
}
@Override
public int getOrdinal() {
return ORDINAL;
}
@Override
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_hid;
}
@Override
public int getSummaryResourceForDevice(BluetoothDevice device) {
final int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_hid_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_hid_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
@Override
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_misc_hid;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.HID_DEVICE,
mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up HID proxy", t);
}
}
}
}

View File

@@ -0,0 +1,202 @@
/*
* Copyright (C) 2012 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHidHost;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import com.android.settingslib.R;
import java.util.List;
/**
* HidProfile handles Bluetooth HID Host role.
*/
public class HidProfile implements LocalBluetoothProfile {
private static final String TAG = "HidProfile";
private BluetoothHidHost mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
static final String NAME = "HID";
// Order of this profile in device profiles list
private static final int ORDINAL = 3;
// These callbacks run on the main thread.
private final class HidHostServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothHidHost) proxy;
// We just bound to the service, so refresh the UI for any connected HID devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "HidProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(HidProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady=true;
}
public void onServiceDisconnected(int profile) {
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.HID_HOST;
}
HidProfile(Context context,
CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new HidHostServiceListener(),
BluetoothProfile.HID_HOST);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) != CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
// TODO: distinguish between keyboard and mouse?
return R.string.bluetooth_profile_hid;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_hid_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_hid_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
if (btClass == null) {
return com.android.internal.R.drawable.ic_lockscreen_ime;
}
return getHidClassDrawable(btClass);
}
public static int getHidClassDrawable(BluetoothClass btClass) {
switch (btClass.getDeviceClass()) {
case BluetoothClass.Device.PERIPHERAL_KEYBOARD:
case BluetoothClass.Device.PERIPHERAL_KEYBOARD_POINTING:
return com.android.internal.R.drawable.ic_lockscreen_ime;
case BluetoothClass.Device.PERIPHERAL_POINTING:
return com.android.internal.R.drawable.ic_bt_pointing_hid;
default:
return com.android.internal.R.drawable.ic_bt_misc_hid;
}
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.HID_HOST,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up HID proxy", t);
}
}
}
}

View File

@@ -0,0 +1,335 @@
/* Copyright 2021 HIMSA II K/S - www.himsa.com. Represented by EHIMA
- www.ehima.com
*/
/* 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothAdapter.ACTIVE_DEVICE_ALL;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
public class LeAudioProfile implements LocalBluetoothProfile {
private static final String TAG = "LeAudioProfile";
private static boolean DEBUG = true;
private Context mContext;
private BluetoothLeAudio mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
static final String NAME = "LE_AUDIO";
private final LocalBluetoothProfileManager mProfileManager;
private final BluetoothAdapter mBluetoothAdapter;
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
// These callbacks run on the main thread.
private final class LeAudioServiceListener implements BluetoothProfile.ServiceListener {
@RequiresApi(Build.VERSION_CODES.S)
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (DEBUG) {
Log.d(TAG, "Bluetooth service connected");
}
mService = (BluetoothLeAudio) proxy;
// We just bound to the service, so refresh the UI for any connected LeAudio devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
if (DEBUG) {
Log.d(TAG, "LeAudioProfile found new device: " + nextDevice);
}
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(LeAudioProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
// Check current list of CachedDevices to see if any are hearing aid devices.
mDeviceManager.updateHearingAidsDevices();
mProfileManager.callServiceConnectedListeners();
mIsProfileReady = true;
}
public void onServiceDisconnected(int profile) {
if (DEBUG) {
Log.d(TAG, "Bluetooth service disconnected");
}
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady = false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.LE_AUDIO;
}
LeAudioProfile(
Context context,
CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mContext = context;
mDeviceManager = deviceManager;
mProfileManager = profileManager;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mBluetoothAdapter.getProfileProxy(
context, new LeAudioServiceListener(), BluetoothProfile.LE_AUDIO);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public List<BluetoothDevice> getConnectedDevices() {
return getDevicesByStates(
new int[] {
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING
});
}
public List<BluetoothDevice> getConnectableDevices() {
return getDevicesByStates(
new int[] {
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING
});
}
private List<BluetoothDevice> getDevicesByStates(int[] states) {
if (mService == null) {
return new ArrayList<>(0);
}
return mService.getDevicesMatchingConnectionStates(states);
}
/*
* @hide
*/
public boolean connect(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
}
/*
* @hide
*/
public boolean disconnect(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.setConnectionPolicy(device, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
/** Get group id for {@link BluetoothDevice}. */
public int getGroupId(@NonNull BluetoothDevice device) {
if (mService == null) {
return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
}
return mService.getGroupId(device);
}
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
return device == null
? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_ALL)
: mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_ALL);
}
public List<BluetoothDevice> getActiveDevices() {
if (mBluetoothAdapter == null) {
return new ArrayList<>();
}
return mBluetoothAdapter.getActiveDevices(BluetoothProfile.LE_AUDIO);
}
/**
* Get Lead device for the group.
*
* <p>Lead device is the device that can be used as an active device in the system. Active
* devices points to the Audio Device for the Le Audio group. This method returns the Lead
* devices for the connected LE Audio group and this device should be used in the
* setActiveDevice() method by other parts of the system, which wants to set to active a
* particular Le Audio group.
*
* <p>Note: getActiveDevice() returns the Lead device for the currently active LE Audio group.
* Note: When Lead device gets disconnected while Le Audio group is active and has more devices
* in the group, then Lead device will not change. If Lead device gets disconnected, for the Le
* Audio group which is not active, a new Lead device will be chosen
*
* @param groupId The group id.
* @return group lead device.
* @hide
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public @Nullable BluetoothDevice getConnectedGroupLeadDevice(int groupId) {
if (DEBUG) {
Log.d(TAG, "getConnectedGroupLeadDevice");
}
if (mService == null) {
Log.e(TAG, "No service.");
return null;
}
return mService.getConnectedGroupLeadDevice(groupId);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null || device == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null || device == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null || device == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_le_audio;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_le_audio_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_le_audio_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
if (btClass == null) {
Log.e(TAG, "No btClass.");
return R.drawable.ic_bt_le_audio_speakers;
}
switch (btClass.getDeviceClass()) {
case BluetoothClass.Device.AUDIO_VIDEO_UNCATEGORIZED:
case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
case BluetoothClass.Device.AUDIO_VIDEO_MICROPHONE:
case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES:
return R.drawable.ic_bt_le_audio;
default:
return R.drawable.ic_bt_le_audio_speakers;
}
}
public int getAudioLocation(BluetoothDevice device) {
if (mService == null || device == null) {
return BluetoothLeAudio.AUDIO_LOCATION_INVALID;
}
return mService.getAudioLocation(device);
}
@RequiresApi(Build.VERSION_CODES.S)
protected void finalize() {
if (DEBUG) {
Log.d(TAG, "finalize()");
}
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter()
.closeProfileProxy(BluetoothProfile.LE_AUDIO, mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up LeAudio proxy", t);
}
}
}
}

View File

@@ -0,0 +1,262 @@
/*
* 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.settingslib.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
import android.bluetooth.le.BluetoothLeScanner;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import java.time.Duration;
import java.util.List;
import java.util.Set;
/**
* LocalBluetoothAdapter provides an interface between the Settings app
* and the functionality of the local {@link BluetoothAdapter}, specifically
* those related to state transitions of the adapter itself.
*
* <p>Connection and bonding state changes affecting specific devices
* are handled by {@link CachedBluetoothDeviceManager},
* {@link BluetoothEventManager}, and {@link LocalBluetoothProfileManager}.
*
* @deprecated use {@link BluetoothAdapter} instead.
*/
@Deprecated
public class LocalBluetoothAdapter {
private static final String TAG = "LocalBluetoothAdapter";
/** This class does not allow direct access to the BluetoothAdapter. */
private final BluetoothAdapter mAdapter;
private LocalBluetoothProfileManager mProfileManager;
private static LocalBluetoothAdapter sInstance;
private int mState = BluetoothAdapter.ERROR;
private static final int SCAN_EXPIRATION_MS = 5 * 60 * 1000; // 5 mins
private long mLastScan;
private LocalBluetoothAdapter(BluetoothAdapter adapter) {
mAdapter = adapter;
}
void setProfileManager(LocalBluetoothProfileManager manager) {
mProfileManager = manager;
}
/**
* Get the singleton instance of the LocalBluetoothAdapter. If this device
* doesn't support Bluetooth, then null will be returned. Callers must be
* prepared to handle a null return value.
* @return the LocalBluetoothAdapter object, or null if not supported
*/
static synchronized LocalBluetoothAdapter getInstance() {
if (sInstance == null) {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter != null) {
sInstance = new LocalBluetoothAdapter(adapter);
}
}
return sInstance;
}
// Pass-through BluetoothAdapter methods that we can intercept if necessary
public void cancelDiscovery() {
mAdapter.cancelDiscovery();
}
public boolean enable() {
return mAdapter.enable();
}
public boolean disable() {
return mAdapter.disable();
}
public String getAddress() {
return mAdapter.getAddress();
}
void getProfileProxy(Context context,
BluetoothProfile.ServiceListener listener, int profile) {
mAdapter.getProfileProxy(context, listener, profile);
}
public Set<BluetoothDevice> getBondedDevices() {
return mAdapter.getBondedDevices();
}
public String getName() {
return mAdapter.getName();
}
public int getScanMode() {
return mAdapter.getScanMode();
}
public BluetoothLeScanner getBluetoothLeScanner() {
return mAdapter.getBluetoothLeScanner();
}
public int getState() {
return mAdapter.getState();
}
public ParcelUuid[] getUuids() {
List<ParcelUuid> uuidsList = mAdapter.getUuidsList();
ParcelUuid[] uuidsArray = new ParcelUuid[uuidsList.size()];
uuidsList.toArray(uuidsArray);
return uuidsArray;
}
public boolean isDiscovering() {
return mAdapter.isDiscovering();
}
public boolean isEnabled() {
return mAdapter.isEnabled();
}
public int getConnectionState() {
return mAdapter.getConnectionState();
}
public void setDiscoverableTimeout(int timeout) {
mAdapter.setDiscoverableTimeout(Duration.ofSeconds(timeout));
}
public long getDiscoveryEndMillis() {
return mAdapter.getDiscoveryEndMillis();
}
public void setName(String name) {
mAdapter.setName(name);
}
public void setScanMode(int mode) {
mAdapter.setScanMode(mode);
}
public boolean setScanMode(int mode, int duration) {
return (mAdapter.setDiscoverableTimeout(Duration.ofSeconds(duration))
== BluetoothStatusCodes.SUCCESS
&& mAdapter.setScanMode(mode) == BluetoothStatusCodes.SUCCESS);
}
public void startScanning(boolean force) {
// Only start if we're not already scanning
if (!mAdapter.isDiscovering()) {
if (!force) {
// Don't scan more than frequently than SCAN_EXPIRATION_MS,
// unless forced
if (mLastScan + SCAN_EXPIRATION_MS > System.currentTimeMillis()) {
return;
}
// If we are playing music, don't scan unless forced.
A2dpProfile a2dp = mProfileManager.getA2dpProfile();
if (a2dp != null && a2dp.isA2dpPlaying()) {
return;
}
A2dpSinkProfile a2dpSink = mProfileManager.getA2dpSinkProfile();
if ((a2dpSink != null) && (a2dpSink.isAudioPlaying())) {
return;
}
}
if (mAdapter.startDiscovery()) {
mLastScan = System.currentTimeMillis();
}
}
}
public void stopScanning() {
if (mAdapter.isDiscovering()) {
mAdapter.cancelDiscovery();
}
}
public synchronized int getBluetoothState() {
// Always sync state, in case it changed while paused
syncBluetoothState();
return mState;
}
void setBluetoothStateInt(int state) {
synchronized(this) {
if (mState == state) {
return;
}
mState = state;
}
if (state == BluetoothAdapter.STATE_ON) {
// if mProfileManager hasn't been constructed yet, it will
// get the adapter UUIDs in its constructor when it is.
if (mProfileManager != null) {
mProfileManager.setBluetoothStateOn();
}
}
}
// Returns true if the state changed; false otherwise.
boolean syncBluetoothState() {
int currentState = mAdapter.getState();
if (currentState != mState) {
setBluetoothStateInt(mAdapter.getState());
return true;
}
return false;
}
public boolean setBluetoothEnabled(boolean enabled) {
boolean success = enabled
? mAdapter.enable()
: mAdapter.disable();
if (success) {
setBluetoothStateInt(enabled
? BluetoothAdapter.STATE_TURNING_ON
: BluetoothAdapter.STATE_TURNING_OFF);
} else {
if (BluetoothUtils.V) {
Log.v(TAG, "setBluetoothEnabled call, manager didn't return " +
"success for enabled: " + enabled);
}
syncBluetoothState();
}
return success;
}
public BluetoothDevice getRemoteDevice(String address) {
return mAdapter.getRemoteDevice(address);
}
public List<Integer> getSupportedProfiles() {
return mAdapter.getSupportedProfiles();
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.settingslib.bluetooth;
import android.bluetooth.BluetoothLeAudioContentMetadata;
public class LocalBluetoothLeAudioContentMetadata {
private static final String TAG = "LocalBluetoothLeAudioContentMetadata";
private final BluetoothLeAudioContentMetadata mContentMetadata;
private final String mLanguage;
private final byte[] mRawMetadata;
private String mProgramInfo;
LocalBluetoothLeAudioContentMetadata(BluetoothLeAudioContentMetadata contentMetadata) {
mContentMetadata = contentMetadata;
mProgramInfo = contentMetadata.getProgramInfo();
mLanguage = contentMetadata.getLanguage();
mRawMetadata = contentMetadata.getRawMetadata();
}
public void setProgramInfo(String programInfo) {
mProgramInfo = programInfo;
}
public String getProgramInfo() {
return mProgramInfo;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,450 @@
/*
* 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.annotation.CallbackExecutor;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothProfile.ServiceListener;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
/**
* LocalBluetoothLeBroadcastAssistant provides an interface between the Settings app and the
* functionality of the local {@link BluetoothLeBroadcastAssistant}. Use the {@link
* BluetoothLeBroadcastAssistant.Callback} to get the result callback.
*/
public class LocalBluetoothLeBroadcastAssistant implements LocalBluetoothProfile {
private static final String TAG = "LocalBluetoothLeBroadcastAssistant";
private static final int UNKNOWN_VALUE_PLACEHOLDER = -1;
private static final boolean DEBUG = BluetoothUtils.D;
static final String NAME = "LE_AUDIO_BROADCAST_ASSISTANT";
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
private LocalBluetoothProfileManager mProfileManager;
private BluetoothLeBroadcastAssistant mService;
private final CachedBluetoothDeviceManager mDeviceManager;
private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata;
private BluetoothLeBroadcastMetadata.Builder mBuilder;
private boolean mIsProfileReady;
// Cached assistant callbacks being register before service is connected.
private final Map<BluetoothLeBroadcastAssistant.Callback, Executor> mCachedCallbackExecutorMap =
new ConcurrentHashMap<>();
private final ServiceListener mServiceListener =
new ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (DEBUG) {
Log.d(TAG, "Bluetooth service connected");
}
mService = (BluetoothLeBroadcastAssistant) proxy;
// We just bound to the service, so refresh the UI for any connected LeAudio
// devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
if (DEBUG) {
Log.d(
TAG,
"LocalBluetoothLeBroadcastAssistant found new device: "
+ nextDevice);
}
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(
LocalBluetoothLeBroadcastAssistant.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mProfileManager.callServiceConnectedListeners();
mIsProfileReady = true;
if (DEBUG) {
Log.d(
TAG,
"onServiceConnected, register mCachedCallbackExecutorMap = "
+ mCachedCallbackExecutorMap);
}
mCachedCallbackExecutorMap.forEach(
(callback, executor) -> registerServiceCallBack(executor, callback));
}
@Override
public void onServiceDisconnected(int profile) {
if (profile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
Log.d(TAG, "The profile is not LE_AUDIO_BROADCAST_ASSISTANT");
return;
}
if (DEBUG) {
Log.d(TAG, "Bluetooth service disconnected");
}
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady = false;
mCachedCallbackExecutorMap.clear();
}
};
public LocalBluetoothLeBroadcastAssistant(
Context context,
CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mProfileManager = profileManager;
mDeviceManager = deviceManager;
BluetoothAdapter.getDefaultAdapter()
.getProfileProxy(
context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
mBuilder = new BluetoothLeBroadcastMetadata.Builder();
}
/**
* Add a Broadcast Source to the Broadcast Sink with {@link BluetoothLeBroadcastMetadata}.
*
* @param sink Broadcast Sink to which the Broadcast Source should be added
* @param metadata Broadcast Source metadata to be added to the Broadcast Sink
* @param isGroupOp {@code true} if Application wants to perform this operation for all
* coordinated set members throughout this session. Otherwise, caller would have to add,
* modify, and remove individual set members.
*/
public void addSource(
BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) {
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
return;
}
mService.addSource(sink, metadata, isGroupOp);
}
/**
* Add a Broadcast Source to the Broadcast Sink with the information which are separated from
* the qr code string.
*
* @param sink Broadcast Sink to which the Broadcast Source should be added
* @param sourceAddressType hardware MAC Address of the device. See {@link
* BluetoothDevice.AddressType}.
* @param presentationDelayMicros presentation delay of this Broadcast Source in microseconds.
* @param sourceAdvertisingSid 1-byte long Advertising_SID of the Broadcast Source.
* @param broadcastId 3-byte long Broadcast_ID of the Broadcast Source.
* @param paSyncInterval Periodic Advertising Sync interval of the broadcast Source, {@link
* BluetoothLeBroadcastMetadata#PA_SYNC_INTERVAL_UNKNOWN} if unknown.
* @param isEncrypted whether the Broadcast Source is encrypted.
* @param broadcastCode Broadcast Code for this Broadcast Source, null if code is not required.
* @param sourceDevice source advertiser address.
* @param isGroupOp {@code true} if Application wants to perform this operation for all
* coordinated set members throughout this session. Otherwise, caller would have to add,
* modify, and remove individual set members.
*/
public void addSource(
@NonNull BluetoothDevice sink,
int sourceAddressType,
int presentationDelayMicros,
int sourceAdvertisingSid,
int broadcastId,
int paSyncInterval,
boolean isEncrypted,
byte[] broadcastCode,
BluetoothDevice sourceDevice,
boolean isGroupOp) {
if (DEBUG) {
Log.d(TAG, "addSource()");
}
buildMetadata(
sourceAddressType,
presentationDelayMicros,
sourceAdvertisingSid,
broadcastId,
paSyncInterval,
isEncrypted,
broadcastCode,
sourceDevice);
addSource(sink, mBluetoothLeBroadcastMetadata, isGroupOp);
}
private void buildMetadata(
int sourceAddressType,
int presentationDelayMicros,
int sourceAdvertisingSid,
int broadcastId,
int paSyncInterval,
boolean isEncrypted,
byte[] broadcastCode,
BluetoothDevice sourceDevice) {
mBluetoothLeBroadcastMetadata =
mBuilder.setSourceDevice(sourceDevice, sourceAddressType)
.setSourceAdvertisingSid(sourceAdvertisingSid)
.setBroadcastId(broadcastId)
.setPaSyncInterval(paSyncInterval)
.setEncrypted(isEncrypted)
.setBroadcastCode(broadcastCode)
.setPresentationDelayMicros(presentationDelayMicros)
.build();
}
public void removeSource(@NonNull BluetoothDevice sink, int sourceId) {
if (DEBUG) {
Log.d(TAG, "removeSource()");
}
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
return;
}
mService.removeSource(sink, sourceId);
}
public void startSearchingForSources(@NonNull List<android.bluetooth.le.ScanFilter> filters) {
if (DEBUG) {
Log.d(TAG, "startSearchingForSources()");
}
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
return;
}
mService.startSearchingForSources(filters);
}
/**
* Return true if a search has been started by this application.
*
* @return true if a search has been started by this application
* @hide
*/
public boolean isSearchInProgress() {
if (DEBUG) {
Log.d(TAG, "isSearchInProgress()");
}
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
return false;
}
return mService.isSearchInProgress();
}
/**
* Stops an ongoing search for nearby Broadcast Sources.
*
* <p>On success, {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopped(int)} will be
* called with reason code {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}. On failure,
* {@link BluetoothLeBroadcastAssistant.Callback#onSearchStopFailed(int)} will be called with
* reason code
*
* @throws IllegalStateException if callback was not registered
*/
public void stopSearchingForSources() {
if (DEBUG) {
Log.d(TAG, "stopSearchingForSources()");
}
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
return;
}
mService.stopSearchingForSources();
}
/**
* Get information about all Broadcast Sources that a Broadcast Sink knows about.
*
* @param sink Broadcast Sink from which to get all Broadcast Sources
* @return the list of Broadcast Receive State {@link BluetoothLeBroadcastReceiveState} stored
* in the Broadcast Sink
* @throws NullPointerException when <var>sink</var> is null
*/
public @NonNull List<BluetoothLeBroadcastReceiveState> getAllSources(
@NonNull BluetoothDevice sink) {
if (DEBUG) {
Log.d(TAG, "getAllSources()");
}
if (mService == null) {
Log.d(TAG, "The BluetoothLeBroadcastAssistant is null");
return new ArrayList<BluetoothLeBroadcastReceiveState>();
}
return mService.getAllSources(sink);
}
/**
* Register Broadcast Assistant Callbacks to track its state and receivers
*
* @param executor Executor object for callback
* @param callback Callback object to be registered
*/
public void registerServiceCallBack(
@NonNull @CallbackExecutor Executor executor,
@NonNull BluetoothLeBroadcastAssistant.Callback callback) {
if (mService == null) {
Log.d(
TAG,
"registerServiceCallBack failed, the BluetoothLeBroadcastAssistant is null.");
mCachedCallbackExecutorMap.putIfAbsent(callback, executor);
return;
}
try {
mService.registerCallback(executor, callback);
} catch (IllegalArgumentException e) {
Log.w(TAG, "registerServiceCallBack failed. " + e.getMessage());
}
}
/**
* Unregister previously registered Broadcast Assistant Callbacks
*
* @param callback Callback object to be unregistered
*/
public void unregisterServiceCallBack(
@NonNull BluetoothLeBroadcastAssistant.Callback callback) {
mCachedCallbackExecutorMap.remove(callback);
if (mService == null) {
Log.d(
TAG,
"unregisterServiceCallBack failed, the BluetoothLeBroadcastAssistant is null.");
return;
}
try {
mService.unregisterCallback(callback);
} catch (IllegalArgumentException e) {
Log.w(TAG, "unregisterServiceCallBack failed. " + e.getMessage());
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
public int getProfileId() {
return BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT;
}
public boolean accessProfileEnabled() {
return false;
}
public boolean isAutoConnectable() {
return true;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
// LE Audio Broadcasts are not connection-oriented.
return mService.getConnectionState(device);
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING
});
}
public boolean isEnabled(BluetoothDevice device) {
if (mService == null || device == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null || device == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isEnabled = false;
if (mService == null || device == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isEnabled = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isEnabled;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.summary_empty;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
return BluetoothUtils.getConnectionStateSummary(state);
}
public int getDrawableResource(BluetoothClass btClass) {
return 0;
}
@RequiresApi(Build.VERSION_CODES.S)
protected void finalize() {
if (DEBUG) {
Log.d(TAG, "finalize()");
}
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter()
.closeProfileProxy(BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up LeAudio proxy", t);
}
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.settingslib.bluetooth
import android.bluetooth.BluetoothLeBroadcastMetadata
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt.toQrCodeString
@Deprecated("Replace with BluetoothLeBroadcastMetadataExt")
class LocalBluetoothLeBroadcastMetadata(private val metadata: BluetoothLeBroadcastMetadata?) {
constructor() : this(null)
fun convertToQrCodeString(): String = metadata?.toQrCodeString() ?: ""
fun convertToBroadcastMetadata(qrCodeString: String) =
BluetoothLeBroadcastMetadataExt.convertToBroadcastMetadata(qrCodeString)
}

View File

@@ -0,0 +1,169 @@
/*
* 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.settingslib.bluetooth;
import android.content.Context;
import android.os.Handler;
import android.os.UserHandle;
import android.util.Log;
import java.lang.ref.WeakReference;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
/**
* LocalBluetoothManager provides a simplified interface on top of a subset of
* the Bluetooth API. Note that {@link #getInstance} will return null
* if there is no Bluetooth adapter on this device, and callers must be
* prepared to handle this case.
*/
public class LocalBluetoothManager {
private static final String TAG = "LocalBluetoothManager";
/** Singleton instance. */
private static LocalBluetoothManager sInstance;
private final Context mContext;
/** If a BT-related activity is in the foreground, this will be it. */
private WeakReference<Context> mForegroundActivity;
private final LocalBluetoothAdapter mLocalAdapter;
private final CachedBluetoothDeviceManager mCachedDeviceManager;
/** The Bluetooth profile manager. */
private final LocalBluetoothProfileManager mProfileManager;
/** The broadcast receiver event manager. */
private final BluetoothEventManager mEventManager;
@Nullable
public static synchronized LocalBluetoothManager getInstance(Context context,
BluetoothManagerCallback onInitCallback) {
if (sInstance == null) {
LocalBluetoothAdapter adapter = LocalBluetoothAdapter.getInstance();
if (adapter == null) {
return null;
}
// This will be around as long as this process is
sInstance = new LocalBluetoothManager(adapter, context, /* handler= */ null,
/* userHandle= */ null);
if (onInitCallback != null) {
onInitCallback.onBluetoothManagerInitialized(context.getApplicationContext(),
sInstance);
}
}
return sInstance;
}
/**
* Returns a new instance of {@link LocalBluetoothManager} or null if Bluetooth is not
* supported for this hardware. This instance should be globally cached by the caller.
*/
@Nullable
public static LocalBluetoothManager create(Context context, Handler handler) {
LocalBluetoothAdapter adapter = LocalBluetoothAdapter.getInstance();
if (adapter == null) {
return null;
}
return new LocalBluetoothManager(adapter, context, handler, /* userHandle= */ null);
}
/**
* Returns a new instance of {@link LocalBluetoothManager} or null if Bluetooth is not
* supported for this hardware. This instance should be globally cached by the caller.
*
* <p> Allows to specify a {@link UserHandle} for which to receive bluetooth events.
*
* <p> Requires {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission.
*/
@Nullable
@RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
public static LocalBluetoothManager create(Context context, Handler handler,
UserHandle userHandle) {
LocalBluetoothAdapter adapter = LocalBluetoothAdapter.getInstance();
if (adapter == null) {
return null;
}
return new LocalBluetoothManager(adapter, context, handler,
userHandle);
}
private LocalBluetoothManager(LocalBluetoothAdapter adapter, Context context, Handler handler,
UserHandle userHandle) {
mContext = context.getApplicationContext();
mLocalAdapter = adapter;
mCachedDeviceManager = new CachedBluetoothDeviceManager(mContext, this);
mEventManager = new BluetoothEventManager(mLocalAdapter, mCachedDeviceManager, mContext,
handler, userHandle);
mProfileManager = new LocalBluetoothProfileManager(mContext,
mLocalAdapter, mCachedDeviceManager, mEventManager);
mProfileManager.updateLocalProfiles();
mEventManager.readPairedDevices();
}
public LocalBluetoothAdapter getBluetoothAdapter() {
return mLocalAdapter;
}
public Context getContext() {
return mContext;
}
public Context getForegroundActivity() {
return mForegroundActivity == null
? null
: mForegroundActivity.get();
}
public boolean isForegroundActivity() {
return mForegroundActivity != null && mForegroundActivity.get() != null;
}
public synchronized void setForegroundActivity(Context context) {
if (context != null) {
Log.d(TAG, "setting foreground activity to non-null context");
mForegroundActivity = new WeakReference<>(context);
} else {
if (mForegroundActivity != null) {
Log.d(TAG, "setting foreground activity to null");
mForegroundActivity = null;
}
}
}
public CachedBluetoothDeviceManager getCachedDeviceManager() {
return mCachedDeviceManager;
}
public BluetoothEventManager getEventManager() {
return mEventManager;
}
public LocalBluetoothProfileManager getProfileManager() {
return mProfileManager;
}
public interface BluetoothManagerCallback {
void onBluetoothManagerInitialized(Context appContext,
LocalBluetoothManager bluetoothManager);
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2024 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.settingslib.bluetooth
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
/** Returns a [Flow] that emits a [Unit] whenever the headset audio mode changes. */
val LocalBluetoothManager.headsetAudioModeChanges: Flow<Unit>
get() {
return callbackFlow {
val callback =
object : BluetoothCallback {
override fun onAudioModeChanged() {
launch { send(Unit) }
}
}
eventManager.registerCallback(callback)
awaitClose { eventManager.unregisterCallback(callback) }
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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.settingslib.bluetooth;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
/**
* LocalBluetoothProfile is an interface defining the basic
* functionality related to a Bluetooth profile.
*/
public interface LocalBluetoothProfile {
/**
* Return {@code true} if the user can initiate a connection for this profile in UI.
*/
boolean accessProfileEnabled();
/**
* Returns true if the user can enable auto connection for this profile.
*/
boolean isAutoConnectable();
int getConnectionStatus(BluetoothDevice device);
/**
* Return {@code true} if the profile is enabled, otherwise return {@code false}.
* @param device the device to query for enable status
*/
boolean isEnabled(BluetoothDevice device);
/**
* Get the connection policy of the profile.
* @param device the device to query for enable status
*/
int getConnectionPolicy(BluetoothDevice device);
/**
* Enable the profile if {@code enabled} is {@code true}, otherwise disable profile.
* @param device the device to set profile status
* @param enabled {@code true} for enable profile, otherwise disable profile.
*/
boolean setEnabled(BluetoothDevice device, boolean enabled);
boolean isProfileReady();
int getProfileId();
/** Display order for device profile settings. */
int getOrdinal();
/**
* Returns the string resource ID for the localized name for this profile.
* @param device the Bluetooth device (to distinguish between PAN roles)
*/
int getNameResource(BluetoothDevice device);
/**
* Returns the string resource ID for the summary text for this profile
* for the specified device, e.g. "Use for media audio" or
* "Connected to media audio".
* @param device the device to query for profile connection status
* @return a string resource ID for the profile summary text
*/
int getSummaryResourceForDevice(BluetoothDevice device);
int getDrawableResource(BluetoothClass btClass);
}

View File

@@ -0,0 +1,737 @@
/*
* 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.settingslib.bluetooth;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothA2dpSink;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothCsipSetCoordinator;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHapClient;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHeadsetClient;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothHidDevice;
import android.bluetooth.BluetoothHidHost;
import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothLeBroadcastAssistant;
import android.bluetooth.BluetoothMap;
import android.bluetooth.BluetoothMapClient;
import android.bluetooth.BluetoothPan;
import android.bluetooth.BluetoothPbap;
import android.bluetooth.BluetoothPbapClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothSap;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.BluetoothVolumeControl;
import android.content.Context;
import android.content.Intent;
import android.os.ParcelUuid;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* LocalBluetoothProfileManager provides access to the LocalBluetoothProfile
* objects for the available Bluetooth profiles.
*/
public class LocalBluetoothProfileManager {
private static final String TAG = "LocalBluetoothProfileManager";
private static final boolean DEBUG = BluetoothUtils.D;
/**
* An interface for notifying BluetoothHeadset IPC clients when they have
* been connected to the BluetoothHeadset service.
* Only used by com.android.settings.bluetooth.DockService.
*/
public interface ServiceListener {
/**
* Called to notify the client when this proxy object has been
* connected to the BluetoothHeadset service. Clients must wait for
* this callback before making IPC calls on the BluetoothHeadset
* service.
*/
void onServiceConnected();
/**
* Called to notify the client that this proxy object has been
* disconnected from the BluetoothHeadset service. Clients must not
* make IPC calls on the BluetoothHeadset service after this callback.
* This callback will currently only occur if the application hosting
* the BluetoothHeadset service, but may be called more often in future.
*/
void onServiceDisconnected();
}
private final Context mContext;
private final CachedBluetoothDeviceManager mDeviceManager;
private final BluetoothEventManager mEventManager;
private A2dpProfile mA2dpProfile;
private A2dpSinkProfile mA2dpSinkProfile;
private HeadsetProfile mHeadsetProfile;
private HfpClientProfile mHfpClientProfile;
private MapProfile mMapProfile;
private MapClientProfile mMapClientProfile;
private HidProfile mHidProfile;
private HidDeviceProfile mHidDeviceProfile;
private OppProfile mOppProfile;
private PanProfile mPanProfile;
private PbapClientProfile mPbapClientProfile;
private PbapServerProfile mPbapProfile;
private HearingAidProfile mHearingAidProfile;
private HapClientProfile mHapClientProfile;
private CsipSetCoordinatorProfile mCsipSetCoordinatorProfile;
private LeAudioProfile mLeAudioProfile;
private LocalBluetoothLeBroadcast mLeAudioBroadcast;
private LocalBluetoothLeBroadcastAssistant mLeAudioBroadcastAssistant;
private SapProfile mSapProfile;
private VolumeControlProfile mVolumeControlProfile;
/**
* Mapping from profile name, e.g. "HEADSET" to profile object.
*/
private final Map<String, LocalBluetoothProfile>
mProfileNameMap = new HashMap<String, LocalBluetoothProfile>();
LocalBluetoothProfileManager(Context context,
LocalBluetoothAdapter adapter,
CachedBluetoothDeviceManager deviceManager,
BluetoothEventManager eventManager) {
mContext = context;
mDeviceManager = deviceManager;
mEventManager = eventManager;
// pass this reference to adapter and event manager (circular dependency)
adapter.setProfileManager(this);
if (DEBUG) Log.d(TAG, "LocalBluetoothProfileManager construction complete");
}
/**
* create profile instance according to bluetooth supported profile list
*/
synchronized void updateLocalProfiles() {
List<Integer> supportedList = BluetoothAdapter.getDefaultAdapter().getSupportedProfiles();
if (CollectionUtils.isEmpty(supportedList)) {
if (DEBUG) Log.d(TAG, "supportedList is null");
return;
}
if (mA2dpProfile == null && supportedList.contains(BluetoothProfile.A2DP)) {
if (DEBUG) Log.d(TAG, "Adding local A2DP profile");
mA2dpProfile = new A2dpProfile(mContext, mDeviceManager, this);
addProfile(mA2dpProfile, A2dpProfile.NAME,
BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
}
if (mA2dpSinkProfile == null && supportedList.contains(BluetoothProfile.A2DP_SINK)) {
if (DEBUG) Log.d(TAG, "Adding local A2DP SINK profile");
mA2dpSinkProfile = new A2dpSinkProfile(mContext, mDeviceManager, this);
addProfile(mA2dpSinkProfile, A2dpSinkProfile.NAME,
BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED);
}
if (mHeadsetProfile == null && supportedList.contains(BluetoothProfile.HEADSET)) {
if (DEBUG) Log.d(TAG, "Adding local HEADSET profile");
mHeadsetProfile = new HeadsetProfile(mContext, mDeviceManager, this);
addHeadsetProfile(mHeadsetProfile, HeadsetProfile.NAME,
BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED,
BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED,
BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
}
if (mHfpClientProfile == null && supportedList.contains(BluetoothProfile.HEADSET_CLIENT)) {
if (DEBUG) Log.d(TAG, "Adding local HfpClient profile");
mHfpClientProfile = new HfpClientProfile(mContext, mDeviceManager, this);
addProfile(mHfpClientProfile, HfpClientProfile.NAME,
BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED);
}
if (mMapClientProfile == null && supportedList.contains(BluetoothProfile.MAP_CLIENT)) {
if (DEBUG) Log.d(TAG, "Adding local MAP CLIENT profile");
mMapClientProfile = new MapClientProfile(mContext, mDeviceManager,this);
addProfile(mMapClientProfile, MapClientProfile.NAME,
BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED);
}
if (mMapProfile == null && supportedList.contains(BluetoothProfile.MAP)) {
if (DEBUG) Log.d(TAG, "Adding local MAP profile");
mMapProfile = new MapProfile(mContext, mDeviceManager, this);
addProfile(mMapProfile, MapProfile.NAME, BluetoothMap.ACTION_CONNECTION_STATE_CHANGED);
}
if (mOppProfile == null && supportedList.contains(BluetoothProfile.OPP)) {
if (DEBUG) Log.d(TAG, "Adding local OPP profile");
mOppProfile = new OppProfile();
// Note: no event handler for OPP, only name map.
mProfileNameMap.put(OppProfile.NAME, mOppProfile);
}
if (mHearingAidProfile == null && supportedList.contains(BluetoothProfile.HEARING_AID)) {
if (DEBUG) Log.d(TAG, "Adding local Hearing Aid profile");
mHearingAidProfile = new HearingAidProfile(mContext, mDeviceManager,
this);
addProfile(mHearingAidProfile, HearingAidProfile.NAME,
BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
}
if (mHapClientProfile == null && supportedList.contains(BluetoothProfile.HAP_CLIENT)) {
if (DEBUG) Log.d(TAG, "Adding local HAP_CLIENT profile");
mHapClientProfile = new HapClientProfile(mContext, mDeviceManager, this);
addProfile(mHapClientProfile, HapClientProfile.NAME,
BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED);
}
if (mHidProfile == null && supportedList.contains(BluetoothProfile.HID_HOST)) {
if (DEBUG) Log.d(TAG, "Adding local HID_HOST profile");
mHidProfile = new HidProfile(mContext, mDeviceManager, this);
addProfile(mHidProfile, HidProfile.NAME,
BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED);
}
if (mHidDeviceProfile == null && supportedList.contains(BluetoothProfile.HID_DEVICE)) {
if (DEBUG) Log.d(TAG, "Adding local HID_DEVICE profile");
mHidDeviceProfile = new HidDeviceProfile(mContext, mDeviceManager, this);
addProfile(mHidDeviceProfile, HidDeviceProfile.NAME,
BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED);
}
if (mPanProfile == null && supportedList.contains(BluetoothProfile.PAN)) {
if (DEBUG) Log.d(TAG, "Adding local PAN profile");
mPanProfile = new PanProfile(mContext);
addPanProfile(mPanProfile, PanProfile.NAME,
BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
}
if (mPbapProfile == null && supportedList.contains(BluetoothProfile.PBAP)) {
if (DEBUG) Log.d(TAG, "Adding local PBAP profile");
mPbapProfile = new PbapServerProfile(mContext);
addProfile(mPbapProfile, PbapServerProfile.NAME,
BluetoothPbap.ACTION_CONNECTION_STATE_CHANGED);
}
if (mPbapClientProfile == null && supportedList.contains(BluetoothProfile.PBAP_CLIENT)) {
if (DEBUG) Log.d(TAG, "Adding local PBAP Client profile");
mPbapClientProfile = new PbapClientProfile(mContext, mDeviceManager,this);
addProfile(mPbapClientProfile, PbapClientProfile.NAME,
BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED);
}
if (mSapProfile == null && supportedList.contains(BluetoothProfile.SAP)) {
if (DEBUG) {
Log.d(TAG, "Adding local SAP profile");
}
mSapProfile = new SapProfile(mContext, mDeviceManager, this);
addProfile(mSapProfile, SapProfile.NAME, BluetoothSap.ACTION_CONNECTION_STATE_CHANGED);
}
if (mVolumeControlProfile == null
&& supportedList.contains(BluetoothProfile.VOLUME_CONTROL)) {
if (DEBUG) {
Log.d(TAG, "Adding local Volume Control profile");
}
mVolumeControlProfile = new VolumeControlProfile(mContext, mDeviceManager, this);
addProfile(mVolumeControlProfile, VolumeControlProfile.NAME,
BluetoothVolumeControl.ACTION_CONNECTION_STATE_CHANGED);
}
if (mLeAudioProfile == null && supportedList.contains(BluetoothProfile.LE_AUDIO)) {
if (DEBUG) {
Log.d(TAG, "Adding local LE_AUDIO profile");
}
mLeAudioProfile = new LeAudioProfile(mContext, mDeviceManager, this);
addProfile(mLeAudioProfile, LeAudioProfile.NAME,
BluetoothLeAudio.ACTION_LE_AUDIO_CONNECTION_STATE_CHANGED);
}
if (mLeAudioBroadcast == null
&& supportedList.contains(BluetoothProfile.LE_AUDIO_BROADCAST)) {
if (DEBUG) {
Log.d(TAG, "Adding local LE_AUDIO_BROADCAST profile");
}
mLeAudioBroadcast = new LocalBluetoothLeBroadcast(mContext, mDeviceManager);
// no event handler for the LE boradcast.
mProfileNameMap.put(LocalBluetoothLeBroadcast.NAME, mLeAudioBroadcast);
}
if (mLeAudioBroadcastAssistant == null
&& supportedList.contains(BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT)) {
if (DEBUG) {
Log.d(TAG, "Adding local LE_AUDIO_BROADCAST_ASSISTANT profile");
}
mLeAudioBroadcastAssistant = new LocalBluetoothLeBroadcastAssistant(mContext,
mDeviceManager, this);
addProfile(mLeAudioBroadcastAssistant, LocalBluetoothLeBroadcast.NAME,
BluetoothLeBroadcastAssistant.ACTION_CONNECTION_STATE_CHANGED);
}
if (mCsipSetCoordinatorProfile == null
&& supportedList.contains(BluetoothProfile.CSIP_SET_COORDINATOR)) {
if (DEBUG) {
Log.d(TAG, "Adding local CSIP set coordinator profile");
}
mCsipSetCoordinatorProfile =
new CsipSetCoordinatorProfile(mContext, mDeviceManager, this);
addProfile(mCsipSetCoordinatorProfile, mCsipSetCoordinatorProfile.NAME,
BluetoothCsipSetCoordinator.ACTION_CSIS_CONNECTION_STATE_CHANGED);
}
mEventManager.registerProfileIntentReceiver();
}
private void addHeadsetProfile(LocalBluetoothProfile profile, String profileName,
String stateChangedAction, String audioStateChangedAction, int audioDisconnectedState) {
BluetoothEventManager.Handler handler = new HeadsetStateChangeHandler(
profile, audioStateChangedAction, audioDisconnectedState);
mEventManager.addProfileHandler(stateChangedAction, handler);
mEventManager.addProfileHandler(audioStateChangedAction, handler);
mProfileNameMap.put(profileName, profile);
}
private final Collection<ServiceListener> mServiceListeners =
new CopyOnWriteArrayList<ServiceListener>();
private void addProfile(LocalBluetoothProfile profile,
String profileName, String stateChangedAction) {
mEventManager.addProfileHandler(stateChangedAction, new StateChangedHandler(profile));
mProfileNameMap.put(profileName, profile);
}
private void addPanProfile(LocalBluetoothProfile profile,
String profileName, String stateChangedAction) {
mEventManager.addProfileHandler(stateChangedAction,
new PanStateChangedHandler(profile));
mProfileNameMap.put(profileName, profile);
}
public LocalBluetoothProfile getProfileByName(String name) {
return mProfileNameMap.get(name);
}
// Called from LocalBluetoothAdapter when state changes to ON
void setBluetoothStateOn() {
updateLocalProfiles();
mEventManager.readPairedDevices();
}
/**
* Generic handler for connection state change events for the specified profile.
*/
private class StateChangedHandler implements BluetoothEventManager.Handler {
final LocalBluetoothProfile mProfile;
StateChangedHandler(LocalBluetoothProfile profile) {
mProfile = profile;
}
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
if (cachedDevice == null) {
Log.w(TAG, "StateChangedHandler found new device: " + device);
cachedDevice = mDeviceManager.addDevice(device);
}
onReceiveInternal(intent, cachedDevice);
}
protected void onReceiveInternal(Intent intent, CachedBluetoothDevice cachedDevice) {
int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
int oldState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0);
if (newState == BluetoothProfile.STATE_DISCONNECTED &&
oldState == BluetoothProfile.STATE_CONNECTING) {
Log.i(TAG, "Failed to connect " + mProfile + " device");
}
if (getHearingAidProfile() != null
&& mProfile instanceof HearingAidProfile
&& (newState == BluetoothProfile.STATE_CONNECTED)) {
// Check if the HiSyncID has being initialized
if (cachedDevice.getHiSyncId() == BluetoothHearingAid.HI_SYNC_ID_INVALID) {
long newHiSyncId = getHearingAidProfile().getHiSyncId(cachedDevice.getDevice());
if (newHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) {
final BluetoothDevice device = cachedDevice.getDevice();
final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
.setAshaDeviceSide(getHearingAidProfile().getDeviceSide(device))
.setAshaDeviceMode(getHearingAidProfile().getDeviceMode(device))
.setHiSyncId(newHiSyncId);
cachedDevice.setHearingAidInfo(infoBuilder.build());
}
}
HearingAidStatsLogUtils.logHearingAidInfo(cachedDevice);
}
final boolean isHapClientProfile = getHapClientProfile() != null
&& mProfile instanceof HapClientProfile;
final boolean isLeAudioProfile = getLeAudioProfile() != null
&& mProfile instanceof LeAudioProfile;
final boolean isHapClientOrLeAudioProfile = isHapClientProfile || isLeAudioProfile;
if (isHapClientOrLeAudioProfile && newState == BluetoothProfile.STATE_CONNECTED) {
// Checks if both profiles are connected to the device. Hearing aid info need
// to be retrieved from these profiles separately.
if (cachedDevice.isConnectedLeAudioHearingAidDevice()) {
final BluetoothDevice device = cachedDevice.getDevice();
final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
.setLeAudioLocation(getLeAudioProfile().getAudioLocation(device))
.setHapDeviceType(getHapClientProfile().getHearingAidType(device));
cachedDevice.setHearingAidInfo(infoBuilder.build());
HearingAidStatsLogUtils.logHearingAidInfo(cachedDevice);
}
}
if (getCsipSetCoordinatorProfile() != null
&& mProfile instanceof CsipSetCoordinatorProfile
&& newState == BluetoothProfile.STATE_CONNECTED) {
// Check if the GroupID has being initialized
if (cachedDevice.getGroupId() == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
final Map<Integer, ParcelUuid> groupIdMap = getCsipSetCoordinatorProfile()
.getGroupUuidMapByDevice(cachedDevice.getDevice());
if (groupIdMap != null) {
for (Map.Entry<Integer, ParcelUuid> entry: groupIdMap.entrySet()) {
if (entry.getValue().equals(BluetoothUuid.CAP)) {
cachedDevice.setGroupId(entry.getKey());
break;
}
}
}
}
}
cachedDevice.onProfileStateChanged(mProfile, newState);
// Dispatch profile changed after device update
boolean needDispatchProfileConnectionState = true;
if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID
|| cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
needDispatchProfileConnectionState = !mDeviceManager
.onProfileConnectionStateChangedIfProcessed(cachedDevice, newState,
mProfile.getProfileId());
}
if (needDispatchProfileConnectionState) {
cachedDevice.refresh();
mEventManager.dispatchProfileConnectionStateChanged(cachedDevice, newState,
mProfile.getProfileId());
}
}
}
/** Connectivity and audio state change handler for headset profiles. */
private class HeadsetStateChangeHandler extends StateChangedHandler {
private final String mAudioChangeAction;
private final int mAudioDisconnectedState;
HeadsetStateChangeHandler(LocalBluetoothProfile profile, String audioChangeAction,
int audioDisconnectedState) {
super(profile);
mAudioChangeAction = audioChangeAction;
mAudioDisconnectedState = audioDisconnectedState;
}
@Override
public void onReceiveInternal(Intent intent, CachedBluetoothDevice cachedDevice) {
if (mAudioChangeAction.equals(intent.getAction())) {
int newState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
if (newState != mAudioDisconnectedState) {
cachedDevice.onProfileStateChanged(mProfile, BluetoothProfile.STATE_CONNECTED);
}
cachedDevice.refresh();
} else {
super.onReceiveInternal(intent, cachedDevice);
}
}
}
/** State change handler for NAP and PANU profiles. */
private class PanStateChangedHandler extends StateChangedHandler {
PanStateChangedHandler(LocalBluetoothProfile profile) {
super(profile);
}
@Override
public void onReceive(Context context, Intent intent, BluetoothDevice device) {
PanProfile panProfile = (PanProfile) mProfile;
int role = intent.getIntExtra(BluetoothPan.EXTRA_LOCAL_ROLE, 0);
panProfile.setLocalRole(device, role);
super.onReceive(context, intent, device);
}
}
// called from DockService
public void addServiceListener(ServiceListener l) {
mServiceListeners.add(l);
}
// called from DockService
public void removeServiceListener(ServiceListener l) {
mServiceListeners.remove(l);
}
// not synchronized: use only from UI thread! (TODO: verify)
void callServiceConnectedListeners() {
final Collection<ServiceListener> listeners = new ArrayList<>(mServiceListeners);
for (ServiceListener l : listeners) {
l.onServiceConnected();
}
}
// not synchronized: use only from UI thread! (TODO: verify)
void callServiceDisconnectedListeners() {
final Collection<ServiceListener> listeners = new ArrayList<>(mServiceListeners);
for (ServiceListener listener : listeners) {
listener.onServiceDisconnected();
}
}
// This is called by DockService, so check Headset and A2DP.
public synchronized boolean isManagerReady() {
// Getting just the headset profile is fine for now. Will need to deal with A2DP
// and others if they aren't always in a ready state.
LocalBluetoothProfile profile = mHeadsetProfile;
if (profile != null) {
return profile.isProfileReady();
}
profile = mA2dpProfile;
if (profile != null) {
return profile.isProfileReady();
}
profile = mA2dpSinkProfile;
if (profile != null) {
return profile.isProfileReady();
}
return false;
}
public A2dpProfile getA2dpProfile() {
return mA2dpProfile;
}
public A2dpSinkProfile getA2dpSinkProfile() {
if ((mA2dpSinkProfile != null) && (mA2dpSinkProfile.isProfileReady())) {
return mA2dpSinkProfile;
} else {
return null;
}
}
public HeadsetProfile getHeadsetProfile() {
return mHeadsetProfile;
}
public HfpClientProfile getHfpClientProfile() {
if ((mHfpClientProfile != null) && (mHfpClientProfile.isProfileReady())) {
return mHfpClientProfile;
} else {
return null;
}
}
public PbapClientProfile getPbapClientProfile() {
return mPbapClientProfile;
}
public PbapServerProfile getPbapProfile(){
return mPbapProfile;
}
public MapProfile getMapProfile(){
return mMapProfile;
}
public MapClientProfile getMapClientProfile() {
return mMapClientProfile;
}
public HearingAidProfile getHearingAidProfile() {
return mHearingAidProfile;
}
public HapClientProfile getHapClientProfile() {
return mHapClientProfile;
}
public LeAudioProfile getLeAudioProfile() {
return mLeAudioProfile;
}
public LocalBluetoothLeBroadcast getLeAudioBroadcastProfile() {
return mLeAudioBroadcast;
}
public LocalBluetoothLeBroadcastAssistant getLeAudioBroadcastAssistantProfile() {
return mLeAudioBroadcastAssistant;
}
SapProfile getSapProfile() {
return mSapProfile;
}
@VisibleForTesting
HidProfile getHidProfile() {
return mHidProfile;
}
@VisibleForTesting
HidDeviceProfile getHidDeviceProfile() {
return mHidDeviceProfile;
}
public CsipSetCoordinatorProfile getCsipSetCoordinatorProfile() {
return mCsipSetCoordinatorProfile;
}
public VolumeControlProfile getVolumeControlProfile() {
return mVolumeControlProfile;
}
/**
* Fill in a list of LocalBluetoothProfile objects that are supported by
* the local device and the remote device.
*
* @param uuids of the remote device
* @param localUuids UUIDs of the local device
* @param profiles The list of profiles to fill
* @param removedProfiles list of profiles that were removed
*/
synchronized void updateProfiles(ParcelUuid[] uuids, ParcelUuid[] localUuids,
Collection<LocalBluetoothProfile> profiles,
Collection<LocalBluetoothProfile> removedProfiles,
boolean isPanNapConnected, BluetoothDevice device) {
// Copy previous profile list into removedProfiles
removedProfiles.clear();
removedProfiles.addAll(profiles);
if (DEBUG) {
Log.d(TAG,"Current Profiles" + profiles.toString());
}
profiles.clear();
if (uuids == null) {
return;
}
// The profiles list's sequence will affect the bluetooth icon at
// BluetoothUtils.getBtClassDrawableWithDescription(Context,CachedBluetoothDevice).
// Moving the LE audio profile to be the first priority if the device supports LE audio.
if (ArrayUtils.contains(uuids, BluetoothUuid.LE_AUDIO) && mLeAudioProfile != null) {
profiles.add(mLeAudioProfile);
removedProfiles.remove(mLeAudioProfile);
}
if (mHeadsetProfile != null) {
if ((ArrayUtils.contains(localUuids, BluetoothUuid.HSP_AG)
&& ArrayUtils.contains(uuids, BluetoothUuid.HSP))
|| (ArrayUtils.contains(localUuids, BluetoothUuid.HFP_AG)
&& ArrayUtils.contains(uuids, BluetoothUuid.HFP))) {
profiles.add(mHeadsetProfile);
removedProfiles.remove(mHeadsetProfile);
}
}
if ((mHfpClientProfile != null) &&
ArrayUtils.contains(uuids, BluetoothUuid.HFP_AG)
&& ArrayUtils.contains(localUuids, BluetoothUuid.HFP)) {
profiles.add(mHfpClientProfile);
removedProfiles.remove(mHfpClientProfile);
}
if (BluetoothUuid.containsAnyUuid(uuids, A2dpProfile.SINK_UUIDS) && mA2dpProfile != null) {
profiles.add(mA2dpProfile);
removedProfiles.remove(mA2dpProfile);
}
if (BluetoothUuid.containsAnyUuid(uuids, A2dpSinkProfile.SRC_UUIDS)
&& mA2dpSinkProfile != null) {
profiles.add(mA2dpSinkProfile);
removedProfiles.remove(mA2dpSinkProfile);
}
if (ArrayUtils.contains(uuids, BluetoothUuid.OBEX_OBJECT_PUSH) && mOppProfile != null) {
profiles.add(mOppProfile);
removedProfiles.remove(mOppProfile);
}
if ((ArrayUtils.contains(uuids, BluetoothUuid.HID)
|| ArrayUtils.contains(uuids, BluetoothUuid.HOGP)) && mHidProfile != null) {
profiles.add(mHidProfile);
removedProfiles.remove(mHidProfile);
}
if (mHidDeviceProfile != null && mHidDeviceProfile.getConnectionStatus(device)
!= BluetoothProfile.STATE_DISCONNECTED) {
profiles.add(mHidDeviceProfile);
removedProfiles.remove(mHidDeviceProfile);
}
if(isPanNapConnected)
if(DEBUG) Log.d(TAG, "Valid PAN-NAP connection exists.");
if ((ArrayUtils.contains(uuids, BluetoothUuid.NAP) && mPanProfile != null)
|| isPanNapConnected) {
profiles.add(mPanProfile);
removedProfiles.remove(mPanProfile);
}
if ((mMapProfile != null) &&
(mMapProfile.getConnectionStatus(device) == BluetoothProfile.STATE_CONNECTED)) {
profiles.add(mMapProfile);
removedProfiles.remove(mMapProfile);
mMapProfile.setEnabled(device, true);
}
if ((mPbapProfile != null) &&
(mPbapProfile.getConnectionStatus(device) == BluetoothProfile.STATE_CONNECTED)) {
profiles.add(mPbapProfile);
removedProfiles.remove(mPbapProfile);
mPbapProfile.setEnabled(device, true);
}
if ((mMapClientProfile != null)
&& BluetoothUuid.containsAnyUuid(uuids, MapClientProfile.UUIDS)) {
profiles.add(mMapClientProfile);
removedProfiles.remove(mMapClientProfile);
}
if ((mPbapClientProfile != null)
&& BluetoothUuid.containsAnyUuid(uuids, PbapClientProfile.SRC_UUIDS)) {
profiles.add(mPbapClientProfile);
removedProfiles.remove(mPbapClientProfile);
}
if (ArrayUtils.contains(uuids, BluetoothUuid.HEARING_AID) && mHearingAidProfile != null) {
profiles.add(mHearingAidProfile);
removedProfiles.remove(mHearingAidProfile);
}
if (mHapClientProfile != null && ArrayUtils.contains(uuids, BluetoothUuid.HAS)) {
profiles.add(mHapClientProfile);
removedProfiles.remove(mHapClientProfile);
}
if (mSapProfile != null && ArrayUtils.contains(uuids, BluetoothUuid.SAP)) {
profiles.add(mSapProfile);
removedProfiles.remove(mSapProfile);
}
if (mVolumeControlProfile != null
&& ArrayUtils.contains(uuids, BluetoothUuid.VOLUME_CONTROL)) {
profiles.add(mVolumeControlProfile);
removedProfiles.remove(mVolumeControlProfile);
}
if (mCsipSetCoordinatorProfile != null
&& ArrayUtils.contains(uuids, BluetoothUuid.COORDINATED_SET)) {
profiles.add(mCsipSetCoordinatorProfile);
removedProfiles.remove(mCsipSetCoordinatorProfile);
}
if (DEBUG) {
Log.d(TAG,"New Profiles" + profiles.toString());
}
}
}

View File

@@ -0,0 +1,207 @@
/*
* Copyright (C) 2012 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothMapClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
/**
* MapClientProfile handles the Bluetooth MAP MCE role.
*/
public final class MapClientProfile implements LocalBluetoothProfile {
private static final String TAG = "MapClientProfile";
private BluetoothMapClient mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
static final ParcelUuid[] UUIDS = {
BluetoothUuid.MAS,
};
static final String NAME = "MAP Client";
// Order of this profile in device profiles list
private static final int ORDINAL = 0;
// These callbacks run on the main thread.
private final class MapClientServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothMapClient) proxy;
// We just bound to the service, so refresh the UI for any connected MAP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "MapProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(MapClientProfile.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mProfileManager.callServiceConnectedListeners();
mIsProfileReady=true;
}
public void onServiceDisconnected(int profile) {
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
Log.d(TAG, "isProfileReady(): "+ mIsProfileReady);
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.MAP_CLIENT;
}
MapClientProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context,
new MapClientServiceListener(), BluetoothProfile.MAP_CLIENT);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_map;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_map_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_map_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_phone;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.MAP_CLIENT,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up MAP Client proxy", t);
}
}
}
}

View File

@@ -0,0 +1,208 @@
/*
* Copyright (C) 2012 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothMap;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
/**
* MapProfile handles the Bluetooth MAP MSE role
*/
public class MapProfile implements LocalBluetoothProfile {
private static final String TAG = "MapProfile";
private BluetoothMap mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
static final ParcelUuid[] UUIDS = {
BluetoothUuid.MAP,
BluetoothUuid.MNS,
BluetoothUuid.MAS,
};
static final String NAME = "MAP";
// Order of this profile in device profiles list
// These callbacks run on the main thread.
private final class MapServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothMap) proxy;
// We just bound to the service, so refresh the UI for any connected MAP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "MapProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(MapProfile.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mProfileManager.callServiceConnectedListeners();
mIsProfileReady=true;
}
public void onServiceDisconnected(int profile) {
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
Log.d(TAG, "isProfileReady(): " + mIsProfileReady);
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.MAP;
}
MapProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new MapServiceListener(),
BluetoothProfile.MAP);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return BluetoothProfile.MAP;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_map;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_map_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_map_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_phone;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.MAP,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up MAP proxy", t);
}
}
}
}

View File

@@ -0,0 +1,12 @@
# Default reviewers for this and subdirectories.
siyuanh@google.com
hughchen@google.com
timhypeng@google.com
robertluo@google.com
songferngwang@google.com
yqian@google.com
chelseahao@google.com
yiyishen@google.com
hahong@google.com
# Emergency approvers in case the above are not available

View File

@@ -0,0 +1,90 @@
/*
* 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.settingslib.bluetooth;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import com.android.settingslib.R;
/**
* OppProfile handles Bluetooth OPP.
*/
final class OppProfile implements LocalBluetoothProfile {
static final String NAME = "OPP";
// Order of this profile in device profiles list
private static final int ORDINAL = 2;
public boolean accessProfileEnabled() {
return false;
}
public boolean isAutoConnectable() {
return false;
}
public int getConnectionStatus(BluetoothDevice device) {
return BluetoothProfile.STATE_DISCONNECTED; // Settings app doesn't handle OPP
}
@Override
public boolean isEnabled(BluetoothDevice device) {
return false;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
return BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; // Settings app doesn't handle OPP
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
return false;
}
public boolean isProfileReady() {
return true;
}
@Override
public int getProfileId() {
return BluetoothProfile.OPP;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_opp;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
return 0; // OPP profile not displayed in UI
}
public int getDrawableResource(BluetoothClass btClass) {
return 0; // no icon for OPP
}
}

View File

@@ -0,0 +1,190 @@
/*
* 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothPan;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.util.Log;
import com.android.settingslib.R;
import java.util.HashMap;
import java.util.List;
/**
* PanProfile handles Bluetooth PAN profile (NAP and PANU).
*/
public class PanProfile implements LocalBluetoothProfile {
private static final String TAG = "PanProfile";
private BluetoothPan mService;
private boolean mIsProfileReady;
// Tethering direction for each device
private final HashMap<BluetoothDevice, Integer> mDeviceRoleMap =
new HashMap<BluetoothDevice, Integer>();
static final String NAME = "PAN";
// Order of this profile in device profiles list
private static final int ORDINAL = 4;
// These callbacks run on the main thread.
private final class PanServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothPan) proxy;
mIsProfileReady=true;
}
public void onServiceDisconnected(int profile) {
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.PAN;
}
PanProfile(Context context) {
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new PanServiceListener(),
BluetoothProfile.PAN);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return false;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
return true;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
return -1;
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
final List<BluetoothDevice> sinks = mService.getConnectedDevices();
if (sinks != null) {
for (BluetoothDevice sink : sinks) {
mService.setConnectionPolicy(sink, CONNECTION_POLICY_FORBIDDEN);
}
}
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
if (isLocalRoleNap(device)) {
return R.string.bluetooth_profile_pan_nap;
} else {
return R.string.bluetooth_profile_pan;
}
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_pan_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
if (isLocalRoleNap(device)) {
return R.string.bluetooth_pan_nap_profile_summary_connected;
} else {
return R.string.bluetooth_pan_user_profile_summary_connected;
}
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_bt_network_pan;
}
// Tethering direction determines UI strings.
void setLocalRole(BluetoothDevice device, int role) {
mDeviceRoleMap.put(device, role);
}
boolean isLocalRoleNap(BluetoothDevice device) {
if (mDeviceRoleMap.containsKey(device)) {
return mDeviceRoleMap.get(device) == BluetoothPan.LOCAL_NAP_ROLE;
} else {
return false;
}
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.PAN, mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up PAN proxy", t);
}
}
}
}

View File

@@ -0,0 +1,202 @@
/*
* Copyright (C) 2016 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothPbapClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public final class PbapClientProfile implements LocalBluetoothProfile {
private static final String TAG = "PbapClientProfile";
private BluetoothPbapClient mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
static final ParcelUuid[] SRC_UUIDS = {
BluetoothUuid.PBAP_PSE,
};
static final String NAME = "PbapClient";
private final LocalBluetoothProfileManager mProfileManager;
// Order of this profile in device profiles list
private static final int ORDINAL = 6;
// These callbacks run on the main thread.
private final class PbapClientServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothPbapClient) proxy;
// We just bound to the service, so refresh the UI for any connected PBAP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "PbapClientProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(PbapClientProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady = true;
}
public void onServiceDisconnected(int profile) {
mIsProfileReady = false;
}
}
private void refreshProfiles() {
Collection<CachedBluetoothDevice> cachedDevices = mDeviceManager.getCachedDevicesCopy();
for (CachedBluetoothDevice device : cachedDevices) {
device.onUuidChanged();
}
}
public boolean pbapClientExists() {
return (mService != null);
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.PBAP_CLIENT;
}
PbapClientProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context,
new PbapClientServiceListener(), BluetoothProfile.PBAP_CLIENT);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
// we need to have same string in UI as the server side.
return R.string.bluetooth_profile_pbap;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
return R.string.bluetooth_profile_pbap_summary;
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_phone;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(
BluetoothProfile.PBAP_CLIENT,mService);
mService = null;
} catch (Throwable t) {
Log.w(TAG, "Error cleaning up PBAP Client proxy", t);
}
}
}
}

View File

@@ -0,0 +1,155 @@
/*
* 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothPbap;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settingslib.R;
/**
* PBAPServer Profile
*/
public class PbapServerProfile implements LocalBluetoothProfile {
private static final String TAG = "PbapServerProfile";
private BluetoothPbap mService;
private boolean mIsProfileReady;
@VisibleForTesting
public static final String NAME = "PBAP Server";
// Order of this profile in device profiles list
private static final int ORDINAL = 6;
// The UUIDs indicate that remote device might access pbap server
static final ParcelUuid[] PBAB_CLIENT_UUIDS = {
BluetoothUuid.HSP,
BluetoothUuid.HFP,
BluetoothUuid.PBAP_PCE
};
// These callbacks run on the main thread.
private final class PbapServiceListener
implements BluetoothProfile.ServiceListener {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothPbap) proxy;
mIsProfileReady=true;
}
@Override
public void onServiceDisconnected(int profile) {
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.PBAP;
}
PbapServerProfile(Context context) {
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new PbapServiceListener(),
BluetoothProfile.PBAP);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return false;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) return BluetoothProfile.STATE_DISCONNECTED;
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
return false;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
return -1;
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (!enabled) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_pbap;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
return R.string.bluetooth_profile_pbap_summary;
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_phone;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.PBAP,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up PBAP proxy", t);
}
}
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothSap;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.os.ParcelUuid;
import android.util.Log;
import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.List;
/**
* SapProfile handles Bluetooth SAP profile.
*/
final class SapProfile implements LocalBluetoothProfile {
private static final String TAG = "SapProfile";
private BluetoothSap mService;
private boolean mIsProfileReady;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
static final ParcelUuid[] UUIDS = {
BluetoothUuid.SAP,
};
static final String NAME = "SAP";
// Order of this profile in device profiles list
private static final int ORDINAL = 10;
// These callbacks run on the main thread.
private final class SapServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mService = (BluetoothSap) proxy;
// We just bound to the service, so refresh the UI for any connected SAP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "SapProfile found new device: " + nextDevice);
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(SapProfile.this,
BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mProfileManager.callServiceConnectedListeners();
mIsProfileReady=true;
}
public void onServiceDisconnected(int profile) {
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady=false;
}
}
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.SAP;
}
SapProfile(Context context, CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new SapServiceListener(),
BluetoothProfile.SAP);
}
public boolean accessProfileEnabled() {
return true;
}
public boolean isAutoConnectable() {
return true;
}
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null) {
return false;
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING});
}
public String toString() {
return NAME;
}
public int getOrdinal() {
return ORDINAL;
}
public int getNameResource(BluetoothDevice device) {
return R.string.bluetooth_profile_sap;
}
public int getSummaryResourceForDevice(BluetoothDevice device) {
int state = getConnectionStatus(device);
switch (state) {
case BluetoothProfile.STATE_DISCONNECTED:
return R.string.bluetooth_sap_profile_summary_use_for;
case BluetoothProfile.STATE_CONNECTED:
return R.string.bluetooth_sap_profile_summary_connected;
default:
return BluetoothUtils.getConnectionStateSummary(state);
}
}
public int getDrawableResource(BluetoothClass btClass) {
return com.android.internal.R.drawable.ic_phone;
}
protected void finalize() {
Log.d(TAG, "finalize()");
if (mService != null) {
try {
BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.SAP,
mService);
mService = null;
}catch (Throwable t) {
Log.w(TAG, "Error cleaning up SAP proxy", t);
}
}
}
}

View File

@@ -0,0 +1,320 @@
/*
* Copyright (C) 2021 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.settingslib.bluetooth;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
import android.annotation.CallbackExecutor;
import android.annotation.IntRange;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothVolumeControl;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/** VolumeControlProfile handles Bluetooth Volume Control Controller role */
public class VolumeControlProfile implements LocalBluetoothProfile {
private static final String TAG = "VolumeControlProfile";
private static boolean DEBUG = true;
static final String NAME = "VCP";
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
private Context mContext;
private final CachedBluetoothDeviceManager mDeviceManager;
private final LocalBluetoothProfileManager mProfileManager;
private BluetoothVolumeControl mService;
private boolean mIsProfileReady;
// These callbacks run on the main thread.
private final class VolumeControlProfileServiceListener
implements BluetoothProfile.ServiceListener {
@RequiresApi(Build.VERSION_CODES.S)
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (DEBUG) {
Log.d(TAG, "Bluetooth service connected");
}
mService = (BluetoothVolumeControl) proxy;
// We just bound to the service, so refresh the UI for any connected
// VolumeControlProfile devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
if (DEBUG) {
Log.d(TAG, "VolumeControlProfile found new device: " + nextDevice);
}
device = mDeviceManager.addDevice(nextDevice);
}
device.onProfileStateChanged(
VolumeControlProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mProfileManager.callServiceConnectedListeners();
mIsProfileReady = true;
}
public void onServiceDisconnected(int profile) {
if (DEBUG) {
Log.d(TAG, "Bluetooth service disconnected");
}
mProfileManager.callServiceDisconnectedListeners();
mIsProfileReady = false;
}
}
VolumeControlProfile(
Context context,
CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mContext = context;
mDeviceManager = deviceManager;
mProfileManager = profileManager;
BluetoothAdapter.getDefaultAdapter()
.getProfileProxy(
context,
new VolumeControlProfile.VolumeControlProfileServiceListener(),
BluetoothProfile.VOLUME_CONTROL);
}
/**
* Registers a {@link BluetoothVolumeControl.Callback} that will be invoked during the operation
* of this profile.
*
* <p>Repeated registration of the same <var>callback</var> object will have no effect after the
* first call to this method, even when the <var>executor</var> is different. API caller would
* have to call {@link #unregisterCallback(BluetoothVolumeControl.Callback)} with the same
* callback object before registering it again.
*
* @param executor an {@link Executor} to execute given callback
* @param callback user implementation of the {@link BluetoothVolumeControl.Callback}
* @throws IllegalArgumentException if a null executor or callback is given
*/
public void registerCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull BluetoothVolumeControl.Callback callback) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot register callback.");
return;
}
mService.registerCallback(executor, callback);
}
/**
* Unregisters the specified {@link BluetoothVolumeControl.Callback}.
*
* <p>The same {@link BluetoothVolumeControl.Callback} object used when calling {@link
* #registerCallback(Executor, BluetoothVolumeControl.Callback)} must be used.
*
* <p>Callbacks are automatically unregistered when application process goes away
*
* @param callback user implementation of the {@link BluetoothVolumeControl.Callback}
* @throws IllegalArgumentException when callback is null or when no callback is registered
*/
public void unregisterCallback(@NonNull BluetoothVolumeControl.Callback callback) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot unregister callback.");
return;
}
mService.unregisterCallback(callback);
}
/**
* Tells the remote device to set a volume offset to the absolute volume.
*
* @param device {@link BluetoothDevice} representing the remote device
* @param volumeOffset volume offset to be set on the remote device
*/
public void setVolumeOffset(
BluetoothDevice device, @IntRange(from = -255, to = 255) int volumeOffset) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot set volume offset.");
return;
}
if (device == null) {
Log.w(TAG, "Device is null. Cannot set volume offset.");
return;
}
mService.setVolumeOffset(device, volumeOffset);
}
/**
* Provides information about the possibility to set volume offset on the remote device. If the
* remote device supports Volume Offset Control Service, it is automatically connected.
*
* @param device {@link BluetoothDevice} representing the remote device
* @return {@code true} if volume offset function is supported and available to use on the
* remote device. When Bluetooth is off, the return value should always be {@code false}.
*/
public boolean isVolumeOffsetAvailable(BluetoothDevice device) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot get is volume offset available.");
return false;
}
if (device == null) {
Log.w(TAG, "Device is null. Cannot get is volume offset available.");
return false;
}
return mService.isVolumeOffsetAvailable(device);
}
/**
* Tells the remote device to set a volume.
*
* @param device {@link BluetoothDevice} representing the remote device
* @param volume volume to be set on the remote device
* @param isGroupOp whether to set the volume to remote devices within the same CSIP group
*/
public void setDeviceVolume(
BluetoothDevice device,
@IntRange(from = 0, to = 255) int volume,
boolean isGroupOp) {
if (mService == null) {
Log.w(TAG, "Proxy not attached to service. Cannot set volume offset.");
return;
}
if (device == null) {
Log.w(TAG, "Device is null. Cannot set volume offset.");
return;
}
mService.setDeviceVolume(device, volume, isGroupOp);
}
@Override
public boolean accessProfileEnabled() {
return false;
}
@Override
public boolean isAutoConnectable() {
return true;
}
/**
* Gets VolumeControlProfile devices matching connection states{ {@code
* BluetoothProfile.STATE_CONNECTED}, {@code BluetoothProfile.STATE_CONNECTING}, {@code
* BluetoothProfile.STATE_DISCONNECTING}}
*
* @return Matching device list
*/
public List<BluetoothDevice> getConnectedDevices() {
if (mService == null) {
return new ArrayList<BluetoothDevice>(0);
}
return mService.getDevicesMatchingConnectionStates(
new int[] {
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING,
BluetoothProfile.STATE_DISCONNECTING
});
}
@Override
public int getConnectionStatus(BluetoothDevice device) {
if (mService == null) {
return BluetoothProfile.STATE_DISCONNECTED;
}
return mService.getConnectionState(device);
}
@Override
public boolean isEnabled(BluetoothDevice device) {
if (mService == null || device == null) {
return false;
}
return mService.getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN;
}
@Override
public int getConnectionPolicy(BluetoothDevice device) {
if (mService == null || device == null) {
return CONNECTION_POLICY_FORBIDDEN;
}
return mService.getConnectionPolicy(device);
}
@Override
public boolean setEnabled(BluetoothDevice device, boolean enabled) {
boolean isSuccessful = false;
if (mService == null || device == null) {
return false;
}
if (DEBUG) {
Log.d(TAG, device.getAnonymizedAddress() + " setEnabled: " + enabled);
}
if (enabled) {
if (mService.getConnectionPolicy(device) < CONNECTION_POLICY_ALLOWED) {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED);
}
} else {
isSuccessful = mService.setConnectionPolicy(device, CONNECTION_POLICY_FORBIDDEN);
}
return isSuccessful;
}
@Override
public boolean isProfileReady() {
return mIsProfileReady;
}
@Override
public int getProfileId() {
return BluetoothProfile.VOLUME_CONTROL;
}
public String toString() {
return NAME;
}
@Override
public int getOrdinal() {
return ORDINAL;
}
@Override
public int getNameResource(BluetoothDevice device) {
return 0; // VCP profile not displayed in UI
}
@Override
public int getSummaryResourceForDevice(BluetoothDevice device) {
return 0; // VCP profile not displayed in UI
}
@Override
public int getDrawableResource(BluetoothClass btClass) {
// no icon for VCP
return 0;
}
}

View File

@@ -0,0 +1,310 @@
/*
* Copyright (C) 2020 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.settingslib.connectivity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.SubsystemRestartTrackingCallback;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.provider.Settings;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
/**
* An interface class to manage connectivity subsystem recovery/restart operations.
*/
public class ConnectivitySubsystemsRecoveryManager {
private static final String TAG = "ConnectivitySubsystemsRecoveryManager";
private final Context mContext;
private final Handler mHandler;
private RecoveryAvailableListener mRecoveryAvailableListener = null;
private static final long RESTART_TIMEOUT_MS = 15_000; // 15 seconds
private WifiManager mWifiManager = null;
private TelephonyManager mTelephonyManager = null;
private final BroadcastReceiver mApmMonitor = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
RecoveryAvailableListener listener = mRecoveryAvailableListener;
if (listener != null) {
listener.onRecoveryAvailableChangeListener(isRecoveryAvailable());
}
}
};
private boolean mApmMonitorRegistered = false;
private boolean mWifiRestartInProgress = false;
private boolean mTelephonyRestartInProgress = false;
private RecoveryStatusCallback mCurrentRecoveryCallback = null;
private final SubsystemRestartTrackingCallback mWifiSubsystemRestartTrackingCallback =
new SubsystemRestartTrackingCallback() {
@Override
public void onSubsystemRestarting() {
// going to do nothing on this - already assuming that subsystem is restarting
}
@Override
public void onSubsystemRestarted() {
mWifiRestartInProgress = false;
stopTrackingWifiRestart();
checkIfAllSubsystemsRestartsAreDone();
}
};
private final MobileTelephonyCallback mTelephonyCallback = new MobileTelephonyCallback();
private class MobileTelephonyCallback extends TelephonyCallback implements
TelephonyCallback.RadioPowerStateListener {
@Override
public void onRadioPowerStateChanged(int state) {
if (!mTelephonyRestartInProgress || mCurrentRecoveryCallback == null) {
stopTrackingTelephonyRestart();
}
if (state == TelephonyManager.RADIO_POWER_ON) {
mTelephonyRestartInProgress = false;
stopTrackingTelephonyRestart();
checkIfAllSubsystemsRestartsAreDone();
}
}
}
public ConnectivitySubsystemsRecoveryManager(@NonNull Context context,
@NonNull Handler handler) {
mContext = context;
mHandler = new Handler(handler.getLooper());
if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI)) {
mWifiManager = mContext.getSystemService(WifiManager.class);
if (mWifiManager == null) {
Log.e(TAG, "WifiManager not available!?");
}
}
if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
if (mTelephonyManager == null) {
Log.e(TAG, "TelephonyManager not available!?");
}
}
}
/**
* A listener which indicates to the caller whether a recovery operation is available across
* the specified technologies.
*
* Set using {@link #setRecoveryAvailableListener(RecoveryAvailableListener)}, cleared
* using {@link #clearRecoveryAvailableListener()}.
*/
public interface RecoveryAvailableListener {
/**
* Called whenever the recovery availability status changes.
*
* @param isAvailable True if recovery is available across ANY of the requested
* technologies, false if recovery is not available across ALL of the
* requested technologies.
*/
void onRecoveryAvailableChangeListener(boolean isAvailable);
}
/**
* Set a {@link RecoveryAvailableListener} to listen to changes in the recovery availability
* operation for the specified technology(ies).
*
* @param listener Listener to be triggered
*/
public void setRecoveryAvailableListener(@NonNull RecoveryAvailableListener listener) {
mHandler.post(() -> {
mRecoveryAvailableListener = listener;
startTrackingRecoveryAvailability();
});
}
/**
* Clear a listener set with
* {@link #setRecoveryAvailableListener(RecoveryAvailableListener)}.
*/
public void clearRecoveryAvailableListener() {
mHandler.post(() -> {
mRecoveryAvailableListener = null;
stopTrackingRecoveryAvailability();
});
}
private boolean isApmEnabled() {
return Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
}
private boolean isWifiEnabled() {
// TODO: this doesn't consider the scan-only mode. I.e. WiFi is "disabled" while location
// mode is enabled. Probably need to reset WiFi in that state as well. Though this may
// appear strange to the user in that they've actually disabled WiFi.
return mWifiManager != null && (mWifiManager.isWifiEnabled()
|| mWifiManager.isWifiApEnabled());
}
/**
* Provide an indication as to whether subsystem recovery is "available" - i.e. will be
* executed if triggered via {@link #triggerSubsystemRestart(String, RecoveryStatusCallback)}.
*
* @return true if a subsystem recovery is available, false otherwise.
*/
public boolean isRecoveryAvailable() {
if (!isApmEnabled()) return true;
// even if APM is enabled we may still have recovery potential if WiFi is enabled
return isWifiEnabled();
}
private void startTrackingRecoveryAvailability() {
if (mApmMonitorRegistered) return;
mContext.registerReceiver(mApmMonitor,
new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED), null, mHandler);
mApmMonitorRegistered = true;
}
private void stopTrackingRecoveryAvailability() {
if (!mApmMonitorRegistered) return;
mContext.unregisterReceiver(mApmMonitor);
mApmMonitorRegistered = false;
}
@VisibleForTesting
void startTrackingWifiRestart() {
if (mWifiManager == null) return;
mWifiManager.registerSubsystemRestartTrackingCallback(new HandlerExecutor(mHandler),
mWifiSubsystemRestartTrackingCallback);
}
@VisibleForTesting
void stopTrackingWifiRestart() {
if (mWifiManager == null) return;
mWifiManager.unregisterSubsystemRestartTrackingCallback(
mWifiSubsystemRestartTrackingCallback);
}
@VisibleForTesting
void startTrackingTelephonyRestart() {
if (mTelephonyManager == null) return;
mTelephonyManager.registerTelephonyCallback(new HandlerExecutor(mHandler),
mTelephonyCallback);
}
@VisibleForTesting
void stopTrackingTelephonyRestart() {
if (mTelephonyManager == null) return;
mTelephonyManager.unregisterTelephonyCallback(mTelephonyCallback);
}
private void checkIfAllSubsystemsRestartsAreDone() {
if (!mWifiRestartInProgress && !mTelephonyRestartInProgress
&& mCurrentRecoveryCallback != null) {
mCurrentRecoveryCallback.onSubsystemRestartOperationEnd();
mCurrentRecoveryCallback = null;
}
}
/**
* Callbacks used with
* {@link #triggerSubsystemRestart(String, RecoveryStatusCallback)} to get
* information about when recovery starts and is completed.
*/
public interface RecoveryStatusCallback {
/**
* Callback for a subsystem restart triggered via
* {@link #triggerSubsystemRestart(String, RecoveryStatusCallback)} - indicates
* that operation has started.
*/
void onSubsystemRestartOperationBegin();
/**
* Callback for a subsystem restart triggered via
* {@link #triggerSubsystemRestart(String, RecoveryStatusCallback)} - indicates
* that operation has ended. Note that subsystems may still take some time to come up to
* full functionality.
*/
void onSubsystemRestartOperationEnd();
}
/**
* Trigger connectivity recovery for all requested technologies.
*
* @param reason An optional reason code to pass through to the technology-specific
* API. May be used to trigger a bug report.
* @param callback Callbacks triggered when recovery status changes.
*/
public void triggerSubsystemRestart(String reason, @NonNull RecoveryStatusCallback callback) {
// TODO: b/183530649 : clean-up or make use of the `reason` argument
mHandler.post(() -> {
boolean someSubsystemRestarted = false;
if (mWifiRestartInProgress) {
Log.e(TAG, "Wifi restart still in progress");
return;
}
if (mTelephonyRestartInProgress) {
Log.e(TAG, "Telephony restart still in progress");
return;
}
if (isWifiEnabled()) {
mWifiManager.restartWifiSubsystem();
mWifiRestartInProgress = true;
someSubsystemRestarted = true;
startTrackingWifiRestart();
}
if (mTelephonyManager != null && !isApmEnabled()) {
if (mTelephonyManager.rebootRadio()) {
mTelephonyRestartInProgress = true;
someSubsystemRestarted = true;
startTrackingTelephonyRestart();
}
}
if (someSubsystemRestarted) {
mCurrentRecoveryCallback = callback;
callback.onSubsystemRestartOperationBegin();
mHandler.postDelayed(() -> {
stopTrackingWifiRestart();
stopTrackingTelephonyRestart();
mWifiRestartInProgress = false;
mTelephonyRestartInProgress = false;
if (mCurrentRecoveryCallback != null) {
mCurrentRecoveryCallback.onSubsystemRestartOperationEnd();
mCurrentRecoveryCallback = null;
}
}, RESTART_TIMEOUT_MS);
}
});
}
}

View File

@@ -0,0 +1,8 @@
# Default reviewers for this and subdirectories.
andychou@google.com
arcwang@google.com
changbetty@google.com
qal@google.com
wengsu@google.com
# Emergency approvers in case the above are not available

View File

@@ -0,0 +1,176 @@
/*
* Copyright (C) 2023 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.settingslib.core;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.EmptySuper;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.os.BuildCompat;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
/**
* A controller that manages event for preference.
*/
public abstract class AbstractPreferenceController {
private static final String TAG = "AbstractPrefController";
protected final Context mContext;
private final DevicePolicyManager mDevicePolicyManager;
public AbstractPreferenceController(Context context) {
mContext = context;
mDevicePolicyManager =
(DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE);
}
/**
* Displays preference in this controller.
*/
public void displayPreference(PreferenceScreen screen) {
final String prefKey = getPreferenceKey();
if (TextUtils.isEmpty(prefKey)) {
Log.w(TAG, "Skipping displayPreference because key is empty:" + getClass().getName());
return;
}
if (isAvailable()) {
setVisible(screen, prefKey, true /* visible */);
if (this instanceof Preference.OnPreferenceChangeListener) {
final Preference preference = screen.findPreference(prefKey);
if (preference != null) {
preference.setOnPreferenceChangeListener(
(Preference.OnPreferenceChangeListener) this);
}
}
} else {
setVisible(screen, prefKey, false /* visible */);
}
}
/**
* Called on view created.
*/
@EmptySuper
public void onViewCreated(@NonNull LifecycleOwner viewLifecycleOwner) {
}
/**
* Updates the current status of preference (summary, switch state, etc)
*/
public void updateState(Preference preference) {
refreshSummary(preference);
}
/**
* Refresh preference summary with getSummary()
*/
protected void refreshSummary(Preference preference) {
if (preference == null) {
return;
}
final CharSequence summary = getSummary();
if (summary == null) {
// Default getSummary returns null. If subclass didn't override this, there is nothing
// we need to do.
return;
}
preference.setSummary(summary);
}
/**
* Returns true if preference is available (should be displayed)
*/
public abstract boolean isAvailable();
/**
* Handles preference tree click
*
* @param preference the preference being clicked
* @return true if click is handled
*/
public boolean handlePreferenceTreeClick(Preference preference) {
return false;
}
/**
* Returns the key for this preference.
*/
public abstract String getPreferenceKey();
/**
* Show/hide a preference.
*/
protected final void setVisible(PreferenceGroup group, String key, boolean isVisible) {
final Preference pref = group.findPreference(key);
if (pref != null) {
pref.setVisible(isVisible);
}
}
/**
* @return a {@link CharSequence} for the summary of the preference.
*/
public CharSequence getSummary() {
return null;
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
protected void replaceEnterpriseStringTitle(PreferenceScreen screen,
String preferenceKey, String overrideKey, int resource) {
if (!BuildCompat.isAtLeastT() || mDevicePolicyManager == null) {
return;
}
Preference preference = screen.findPreference(preferenceKey);
if (preference == null) {
Log.d(TAG, "Could not find enterprise preference " + preferenceKey);
return;
}
preference.setTitle(
mDevicePolicyManager.getResources().getString(overrideKey,
() -> mContext.getString(resource)));
}
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
protected void replaceEnterpriseStringSummary(
PreferenceScreen screen, String preferenceKey, String overrideKey, int resource) {
if (!BuildCompat.isAtLeastT() || mDevicePolicyManager == null) {
return;
}
Preference preference = screen.findPreference(preferenceKey);
if (preference == null) {
Log.d(TAG, "Could not find enterprise preference " + preferenceKey);
return;
}
preference.setSummary(
mDevicePolicyManager.getResources().getString(overrideKey,
() -> mContext.getString(resource)));
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settingslib.core;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
/**
* Interface for {@link AbstractPreferenceController} objects which manage confirmation dialogs
*/
public interface ConfirmationDialogController {
/**
* Returns the key for this preference.
*/
String getPreferenceKey();
/**
* Shows the dialog
* @param preference Preference object relevant to the dialog being shown
*/
void showConfirmationDialog(@Nullable Preference preference);
/**
* Dismiss the dialog managed by this object
*/
void dismissConfirmationDialog();
/**
* @return {@code true} if the dialog is showing
*/
boolean isConfirmationDialogShowing();
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright (C) 2016 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.settingslib.core.instrumentation;
import android.content.Context;
import android.metrics.LogMaker;
import android.text.TextUtils;
import android.util.Pair;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
/**
* {@link LogWriter} that writes data to eventlog.
*/
public class EventLogWriter implements LogWriter {
@Override
public void visible(Context context, int source, int category, int latency) {
final LogMaker logMaker = new LogMaker(category)
.setType(MetricsProto.MetricsEvent.TYPE_OPEN)
.addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, source)
.addTaggedData(MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE,
latency);
MetricsLogger.action(logMaker);
}
@Override
public void hidden(Context context, int category, int visibleTime) {
final LogMaker logMaker = new LogMaker(category)
.setType(MetricsProto.MetricsEvent.TYPE_CLOSE)
.addTaggedData(MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE,
visibleTime);
MetricsLogger.action(logMaker);
}
@Override
public void clicked(int sourceCategory, String key) {
final LogMaker logMaker = new LogMaker(MetricsProto.MetricsEvent.ACTION_SETTINGS_TILE_CLICK)
.setType(MetricsProto.MetricsEvent.TYPE_ACTION);
if (sourceCategory != MetricsProto.MetricsEvent.VIEW_UNKNOWN) {
logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, sourceCategory);
}
if (!TextUtils.isEmpty(key)) {
logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME,
key);
}
MetricsLogger.action(logMaker);
}
@Override
public void changed(int category, String key, int value) {
final LogMaker logMaker = new LogMaker(
MetricsProto.MetricsEvent.ACTION_SETTINGS_PREFERENCE_CHANGE)
.setType(MetricsProto.MetricsEvent.TYPE_ACTION);
if (category != MetricsProto.MetricsEvent.VIEW_UNKNOWN) {
logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, category);
}
if (!TextUtils.isEmpty(key)) {
logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME,
key);
logMaker.addTaggedData(
MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE,
value);
}
MetricsLogger.action(logMaker);
}
@Override
public void action(Context context, int category, Pair<Integer, Object>... taggedData) {
final LogMaker logMaker = new LogMaker(category)
.setType(MetricsProto.MetricsEvent.TYPE_ACTION);
if (taggedData != null) {
for (Pair<Integer, Object> pair : taggedData) {
logMaker.addTaggedData(pair.first, pair.second);
}
}
MetricsLogger.action(logMaker);
}
@Override
public void action(Context context, int category, int value) {
MetricsLogger.action(context, category, value);
}
@Override
public void action(Context context, int category, boolean value) {
MetricsLogger.action(context, category, value);
}
@Override
public void action(Context context, int category, String pkg) {
final LogMaker logMaker = new LogMaker(category)
.setType(MetricsProto.MetricsEvent.TYPE_ACTION)
.setPackageName(pkg);
MetricsLogger.action(logMaker);
}
@Override
public void action(int attribution, int action, int pageId, String key, int value) {
final LogMaker logMaker = new LogMaker(action)
.setType(MetricsProto.MetricsEvent.TYPE_ACTION);
if (attribution != MetricsProto.MetricsEvent.VIEW_UNKNOWN) {
logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, pageId);
}
if (!TextUtils.isEmpty(key)) {
logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME,
key);
logMaker.addTaggedData(
MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE,
value);
}
MetricsLogger.action(logMaker);
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2016 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.settingslib.core.instrumentation;
public interface Instrumentable {
int METRICS_CATEGORY_UNKNOWN = 0;
/**
* Instrumented name for a view as defined in
* {@link com.android.internal.logging.nano.MetricsProto.MetricsEvent}.
*/
int getMetricsCategory();
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2016 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.settingslib.core.instrumentation;
import android.content.Context;
import android.util.Pair;
/**
* Generic log writer interface.
*/
public interface LogWriter {
/**
* Logs a visibility event when view becomes visible.
*/
void visible(Context context, int source, int category, int latency);
/**
* Logs a visibility event when view becomes hidden.
*/
void hidden(Context context, int category, int visibleTime);
/**
* Logs a click event when user click item.
*/
void clicked(int category, String key);
/**
* Logs a value changed event when user changed item value.
*/
void changed(int category, String key, int value);
/**
* Logs an user action.
*/
void action(Context context, int category, Pair<Integer, Object>... taggedData);
/**
* Logs an user action.
*/
void action(Context context, int category, int value);
/**
* Logs an user action.
*/
void action(Context context, int category, boolean value);
/**
* Logs an user action.
*/
void action(Context context, int category, String pkg);
/**
* Generically log action.
*/
void action(int attribution, int action, int pageId, String key, int value);
}

View File

@@ -0,0 +1,244 @@
/*
* Copyright (C) 2016 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.settingslib.core.instrumentation;
import android.app.Activity;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import java.util.ArrayList;
import java.util.List;
/**
* FeatureProvider for metrics.
*/
public class MetricsFeatureProvider {
/**
* The metrics category constant for logging source when a setting fragment is opened.
*/
public static final String EXTRA_SOURCE_METRICS_CATEGORY = ":settings:source_metrics";
protected List<LogWriter> mLoggerWriters;
public MetricsFeatureProvider() {
mLoggerWriters = new ArrayList<>();
installLogWriters();
}
protected void installLogWriters() {
mLoggerWriters.add(new EventLogWriter());
}
/**
* Returns the attribution id for specified activity. If no attribution is set, returns {@link
* SettingsEnums#PAGE_UNKNOWN}.
*
* <p/> Attribution is a {@link SettingsEnums} page id that indicates where the specified
* activity is launched from.
*/
public int getAttribution(Activity activity) {
if (activity == null) {
return SettingsEnums.PAGE_UNKNOWN;
}
final Intent intent = activity.getIntent();
if (intent == null) {
return SettingsEnums.PAGE_UNKNOWN;
}
return intent.getIntExtra(EXTRA_SOURCE_METRICS_CATEGORY,
SettingsEnums.PAGE_UNKNOWN);
}
/**
* Logs an event when target page is visible.
*
* @param source from this page id to target page
* @param category the target page id
* @param latency the latency of target page creation
*/
public void visible(Context context, int source, int category, int latency) {
for (LogWriter writer : mLoggerWriters) {
writer.visible(context, source, category, latency);
}
}
/**
* Logs an event when target page is hidden.
*
* @param category the target page id
* @param visibleTime the time spending on target page since being visible
*/
public void hidden(Context context, int category, int visibleTime) {
for (LogWriter writer : mLoggerWriters) {
writer.hidden(context, category, visibleTime);
}
}
/**
* Logs an event when user click item.
*
* @param category the target page id
* @param key the key id that user clicked
*/
public void clicked(int category, String key) {
for (LogWriter writer : mLoggerWriters) {
writer.clicked(category, key);
}
}
/**
* Logs a value changed event when user changed item value.
*
* @param category the target page id
* @param key the key id that user clicked
* @param value the value that user changed which converted to integer
*/
public void changed(int category, String key, int value) {
for (LogWriter writer : mLoggerWriters) {
writer.changed(category, key, value);
}
}
/**
* Logs a simple action without page id or attribution
*
* @param category the target page
* @param taggedData the data for {@link EventLogWriter}
*/
public void action(Context context, int category, Pair<Integer, Object>... taggedData) {
for (LogWriter writer : mLoggerWriters) {
writer.action(context, category, taggedData);
}
}
/**
* Logs a generic Settings event.
*/
public void action(Context context, int category, String pkg) {
for (LogWriter writer : mLoggerWriters) {
writer.action(context, category, pkg);
}
}
/**
* Logs a generic Settings event.
*/
public void action(int attribution, int action, int pageId, String key, int value) {
for (LogWriter writer : mLoggerWriters) {
writer.action(attribution, action, pageId, key, value);
}
}
public void action(Context context, int category, int value) {
for (LogWriter writer : mLoggerWriters) {
writer.action(context, category, value);
}
}
public void action(Context context, int category, boolean value) {
for (LogWriter writer : mLoggerWriters) {
writer.action(context, category, value);
}
}
public int getMetricsCategory(Object object) {
if (!(object instanceof Instrumentable)) {
return MetricsEvent.VIEW_UNKNOWN;
}
return ((Instrumentable) object).getMetricsCategory();
}
/**
* Logs an event when the preference is clicked.
*
* @return true if the preference is loggable, otherwise false
*/
public boolean logClickedPreference(@NonNull Preference preference, int sourceMetricsCategory) {
if (preference == null) {
return false;
}
return logSettingsTileClick(preference.getKey(), sourceMetricsCategory)
|| logStartedIntent(preference.getIntent(), sourceMetricsCategory)
|| logSettingsTileClick(preference.getFragment(), sourceMetricsCategory);
}
/**
* Logs an event when the intent is started.
*
* @return true if the intent is loggable, otherwise false
*/
public boolean logStartedIntent(Intent intent, int sourceMetricsCategory) {
if (intent == null) {
return false;
}
final ComponentName cn = intent.getComponent();
return logSettingsTileClick(cn != null ? cn.flattenToString() : intent.getAction(),
sourceMetricsCategory);
}
/**
* Logs an event when the intent is started by Profile select dialog.
*
* @return true if the intent is loggable, otherwise false
*/
public boolean logStartedIntentWithProfile(Intent intent, int sourceMetricsCategory,
boolean isWorkProfile) {
if (intent == null) {
return false;
}
final ComponentName cn = intent.getComponent();
final String key = cn != null ? cn.flattenToString() : intent.getAction();
return logSettingsTileClickWithProfile(key, sourceMetricsCategory, isWorkProfile);
}
/**
* Logs an event when the setting key is clicked.
*
* @return true if the key is loggable, otherwise false
*/
public boolean logSettingsTileClick(String logKey, int sourceMetricsCategory) {
if (TextUtils.isEmpty(logKey)) {
// Not loggable
return false;
}
clicked(sourceMetricsCategory, logKey);
return true;
}
/**
* Logs an event when the setting key is clicked with a specific profile from Profile select
* dialog.
*
* @return true if the key is loggable, otherwise false
*/
public boolean logSettingsTileClickWithProfile(String logKey, int sourceMetricsCategory,
boolean isWorkProfile) {
if (TextUtils.isEmpty(logKey)) {
// Not loggable
return false;
}
clicked(sourceMetricsCategory, logKey + (isWorkProfile ? "/work" : "/personal"));
return true;
}
}

View File

@@ -0,0 +1,77 @@
/*
* 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.settingslib.core.instrumentation
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.preference.PreferenceGroupAdapter
import androidx.preference.TwoStatePreference
import androidx.recyclerview.widget.RecyclerView
import com.android.internal.jank.InteractionJankMonitor
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
/**
* Helper class for Settings library to trace jank.
*/
object SettingsJankMonitor {
private val jankMonitor = InteractionJankMonitor.getInstance()
private val scheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
// Switch toggle animation duration is 250ms, and there is also a ripple effect animation when
// clicks, which duration is variable. Use 300ms here to cover.
@VisibleForTesting
const val MONITORED_ANIMATION_DURATION_MS = 300L
/**
* Detects the jank when click on a TwoStatePreference.
*
* @param recyclerView the recyclerView contains the preference
* @param preference the clicked preference
*/
@JvmStatic
fun detectSwitchPreferenceClickJank(
recyclerView: RecyclerView,
preference: TwoStatePreference,
) {
val adapter = recyclerView.adapter as? PreferenceGroupAdapter ?: return
val adapterPosition = adapter.getPreferenceAdapterPosition(preference)
val viewHolder = recyclerView.findViewHolderForAdapterPosition(adapterPosition) ?: return
detectToggleJank(preference.key, viewHolder.itemView)
}
/**
* Detects the animation jank on the given view.
*
* @param tag the tag for jank monitor
* @param view the instrumented view
*/
@JvmStatic
fun detectToggleJank(tag: String?, view: View) {
val builder = InteractionJankMonitor.Configuration.Builder.withView(
InteractionJankMonitor.CUJ_SETTINGS_TOGGLE,
view
)
if (tag != null) {
builder.setTag(tag)
}
if (jankMonitor.begin(builder)) {
scheduledExecutorService.schedule({
jankMonitor.end(InteractionJankMonitor.CUJ_SETTINGS_TOGGLE)
}, MONITORED_ANIMATION_DURATION_MS, TimeUnit.MILLISECONDS)
}
}
}

View File

@@ -0,0 +1,263 @@
/*
* Copyright (C) 2016 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.settingslib.core.instrumentation;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
public class SharedPreferencesLogger implements SharedPreferences {
private static final String LOG_TAG = "SharedPreferencesLogger";
private final String mTag;
private final int mMetricCategory;
private final Context mContext;
private final MetricsFeatureProvider mMetricsFeature;
private final Set<String> mPreferenceKeySet;
public SharedPreferencesLogger(Context context, String tag,
MetricsFeatureProvider metricsFeature) {
this(context, tag, metricsFeature, SettingsEnums.PAGE_UNKNOWN);
}
public SharedPreferencesLogger(Context context, String tag,
MetricsFeatureProvider metricsFeature, int metricCategory) {
mContext = context;
mTag = tag;
mMetricsFeature = metricsFeature;
mMetricCategory = metricCategory;
mPreferenceKeySet = new ConcurrentSkipListSet<>();
}
@Override
public Map<String, ?> getAll() {
return null;
}
@Override
public String getString(String key, @Nullable String defValue) {
return defValue;
}
@Override
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
return defValues;
}
@Override
public int getInt(String key, int defValue) {
return defValue;
}
@Override
public long getLong(String key, long defValue) {
return defValue;
}
@Override
public float getFloat(String key, float defValue) {
return defValue;
}
@Override
public boolean getBoolean(String key, boolean defValue) {
return defValue;
}
@Override
public boolean contains(String key) {
return false;
}
@Override
public Editor edit() {
return new EditorLogger();
}
@Override
public void registerOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) {
}
@Override
public void unregisterOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) {
}
@VisibleForTesting
protected void logValue(String key, Object value) {
logValue(key, value, false /* forceLog */);
}
private void logValue(String key, Object value, boolean forceLog) {
final String prefKey = buildPrefKey(mTag, key);
if (!forceLog && !mPreferenceKeySet.contains(prefKey)) {
// Pref key doesn't exist in set, this is initial display so we skip metrics but
// keeps track of this key.
mPreferenceKeySet.add(prefKey);
return;
}
final int intVal;
if (value instanceof Long) {
final Long longVal = (Long) value;
if (longVal > Integer.MAX_VALUE) {
intVal = Integer.MAX_VALUE;
} else if (longVal < Integer.MIN_VALUE) {
intVal = Integer.MIN_VALUE;
} else {
intVal = longVal.intValue();
}
} else if (value instanceof Integer) {
intVal = (int) value;
} else if (value instanceof Boolean) {
intVal = (Boolean) value ? 1 : 0;
} else if (value instanceof Float) {
final float floatValue = (float) value;
if (floatValue > Integer.MAX_VALUE) {
intVal = Integer.MAX_VALUE;
} else if (floatValue < Integer.MIN_VALUE) {
intVal = Integer.MIN_VALUE;
} else {
intVal = (int) floatValue;
}
} else if (value instanceof String) {
try {
intVal = Integer.parseInt((String) value);
} catch (NumberFormatException e) {
Log.w(LOG_TAG, "Tried to log unloggable object=" + value);
return;
}
} else {
Log.w(LOG_TAG, "Tried to log unloggable object=" + value);
return;
}
// Pref key exists in set, log its change in metrics.
mMetricsFeature.changed(mMetricCategory, key, intVal);
}
@VisibleForTesting
void logPackageName(String key, String value) {
mMetricsFeature.action(mMetricCategory,
SettingsEnums.ACTION_SETTINGS_PREFERENCE_CHANGE,
SettingsEnums.PAGE_UNKNOWN,
key + ":" + value,
0);
}
private void safeLogValue(String key, String value) {
new AsyncPackageCheck().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, key, value);
}
public static String buildPrefKey(String tag, String key) {
return tag + "/" + key;
}
private class AsyncPackageCheck extends AsyncTask<String, Void, Void> {
@Override
protected Void doInBackground(String... params) {
String key = params[0];
String value = params[1];
PackageManager pm = mContext.getPackageManager();
try {
// Check if this might be a component.
ComponentName name = ComponentName.unflattenFromString(value);
if (value != null) {
value = name.getPackageName();
}
} catch (Exception e) {
}
try {
pm.getPackageInfo(value, PackageManager.MATCH_ANY_USER);
logPackageName(key, value);
} catch (PackageManager.NameNotFoundException e) {
// Clearly not a package, and it's unlikely this preference is in prefSet, so
// lets force log it.
logValue(key, value, true /* forceLog */);
}
return null;
}
}
public class EditorLogger implements Editor {
@Override
public Editor putString(String key, @Nullable String value) {
safeLogValue(key, value);
return this;
}
@Override
public Editor putStringSet(String key, @Nullable Set<String> values) {
safeLogValue(key, TextUtils.join(",", values));
return this;
}
@Override
public Editor putInt(String key, int value) {
logValue(key, value);
return this;
}
@Override
public Editor putLong(String key, long value) {
logValue(key, value);
return this;
}
@Override
public Editor putFloat(String key, float value) {
logValue(key, value);
return this;
}
@Override
public Editor putBoolean(String key, boolean value) {
logValue(key, value);
return this;
}
@Override
public Editor remove(String key) {
return this;
}
@Override
public Editor clear() {
return this;
}
@Override
public boolean commit() {
return true;
}
@Override
public void apply() {
}
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2016 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.settingslib.core.instrumentation;
import static com.android.settingslib.core.instrumentation.Instrumentable.METRICS_CATEGORY_UNKNOWN;
import android.app.Activity;
import android.content.Intent;
import android.os.SystemClock;
import androidx.lifecycle.Lifecycle.Event;
import androidx.lifecycle.OnLifecycleEvent;
import com.android.internal.logging.nano.MetricsProto;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnAttach;
/**
* Logs visibility change of a fragment.
*/
public class VisibilityLoggerMixin implements LifecycleObserver, OnAttach {
private static final String TAG = "VisibilityLoggerMixin";
private final int mMetricsCategory;
private MetricsFeatureProvider mMetricsFeature;
private int mSourceMetricsCategory = MetricsProto.MetricsEvent.VIEW_UNKNOWN;
private long mCreationTimestamp;
private long mVisibleTimestamp;
public VisibilityLoggerMixin(int metricsCategory, MetricsFeatureProvider metricsFeature) {
mMetricsCategory = metricsCategory;
mMetricsFeature = metricsFeature;
}
@Override
public void onAttach() {
mCreationTimestamp = SystemClock.elapsedRealtime();
}
@OnLifecycleEvent(Event.ON_RESUME)
public void onResume() {
if (mMetricsFeature == null || mMetricsCategory == METRICS_CATEGORY_UNKNOWN) {
return;
}
mVisibleTimestamp = SystemClock.elapsedRealtime();
if (mCreationTimestamp != 0L) {
final int elapse = (int) (mVisibleTimestamp - mCreationTimestamp);
mMetricsFeature.visible(null /* context */, mSourceMetricsCategory,
mMetricsCategory, elapse);
} else {
mMetricsFeature.visible(null /* context */, mSourceMetricsCategory,
mMetricsCategory, 0);
}
}
@OnLifecycleEvent(Event.ON_PAUSE)
public void onPause() {
mCreationTimestamp = 0;
if (mMetricsFeature != null && mMetricsCategory != METRICS_CATEGORY_UNKNOWN) {
final int elapse = (int) (SystemClock.elapsedRealtime() - mVisibleTimestamp);
mMetricsFeature.hidden(null /* context */, mMetricsCategory, elapse);
}
}
/**
* Logs the elapsed time from onAttach to calling {@link #writeElapsedTimeMetric(int, String)}.
* @param action : The value of the Action Enums.
* @param key : The value of special key string.
*/
public void writeElapsedTimeMetric(int action, String key) {
if (mMetricsFeature == null || mMetricsCategory == METRICS_CATEGORY_UNKNOWN) {
return;
}
if (mCreationTimestamp != 0L) {
final int elapse = (int) (SystemClock.elapsedRealtime() - mCreationTimestamp);
mMetricsFeature.action(METRICS_CATEGORY_UNKNOWN, action, mMetricsCategory, key, elapse);
}
}
/**
* Sets source metrics category for this logger. Source is the caller that opened this UI.
*/
public void setSourceMetricsCategory(Activity activity) {
if (mSourceMetricsCategory != MetricsProto.MetricsEvent.VIEW_UNKNOWN || activity == null) {
return;
}
final Intent intent = activity.getIntent();
if (intent == null) {
return;
}
mSourceMetricsCategory = intent.getIntExtra(
MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY,
MetricsProto.MetricsEvent.VIEW_UNKNOWN);
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2021 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.settingslib.core.lifecycle;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
import static androidx.lifecycle.Lifecycle.Event.ON_START;
import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
import android.app.Activity;
import android.provider.Settings;
import android.view.Window;
import android.view.WindowManager;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
/**
* A mixin that adds window flag to prevent non-system overlays showing on top of Settings
* activities.
*/
public class HideNonSystemOverlayMixin implements LifecycleObserver {
public static final String SECURE_OVERLAY_SETTINGS = "secure_overlay_settings";
private final Activity mActivity;
public HideNonSystemOverlayMixin(Activity activity) {
mActivity = activity;
}
@VisibleForTesting
boolean isEnabled() {
return Settings.Secure.getInt(mActivity.getContentResolver(),
SECURE_OVERLAY_SETTINGS, 0 /* defValue */) == 0;
}
/**
* Start Lifecycle event
*/
@OnLifecycleEvent(ON_START)
public void onStart() {
if (mActivity == null || !isEnabled()) {
return;
}
mActivity.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
android.util.EventLog.writeEvent(0x534e4554, "120484087", -1, "");
}
/**
* Stop Lifecycle event
*/
@OnLifecycleEvent(ON_STOP)
public void onStop() {
if (mActivity == null || !isEnabled()) {
return;
}
final Window window = mActivity.getWindow();
final WindowManager.LayoutParams attrs = window.getAttributes();
attrs.privateFlags &= ~SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
window.setAttributes(attrs);
}
}

View File

@@ -0,0 +1,242 @@
/*
* Copyright (C) 2016 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.settingslib.core.lifecycle;
import static androidx.lifecycle.Lifecycle.Event.ON_ANY;
import android.annotation.UiThread;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.preference.PreferenceScreen;
import com.android.settingslib.core.lifecycle.events.OnAttach;
import com.android.settingslib.core.lifecycle.events.OnCreate;
import com.android.settingslib.core.lifecycle.events.OnCreateOptionsMenu;
import com.android.settingslib.core.lifecycle.events.OnDestroy;
import com.android.settingslib.core.lifecycle.events.OnOptionsItemSelected;
import com.android.settingslib.core.lifecycle.events.OnPause;
import com.android.settingslib.core.lifecycle.events.OnPrepareOptionsMenu;
import com.android.settingslib.core.lifecycle.events.OnResume;
import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import com.android.settingslib.core.lifecycle.events.SetPreferenceScreen;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Dispatcher for lifecycle events.
*/
public class Lifecycle extends LifecycleRegistry {
private static final String TAG = "LifecycleObserver";
private final List<LifecycleObserver> mObservers = new ArrayList<>();
private final LifecycleProxy mProxy = new LifecycleProxy();
/**
* Creates a new LifecycleRegistry for the given provider.
* <p>
* You should usually create this inside your LifecycleOwner class's constructor and hold
* onto the same instance.
*
* @param provider The owner LifecycleOwner
*/
public Lifecycle(@NonNull LifecycleOwner provider) {
super(provider);
addObserver(mProxy);
}
/**
* Registers a new observer of lifecycle events.
*/
@UiThread
@Override
public void addObserver(androidx.lifecycle.LifecycleObserver observer) {
ThreadUtils.ensureMainThread();
super.addObserver(observer);
if (observer instanceof LifecycleObserver) {
mObservers.add((LifecycleObserver) observer);
}
}
@UiThread
@Override
public void removeObserver(androidx.lifecycle.LifecycleObserver observer) {
ThreadUtils.ensureMainThread();
super.removeObserver(observer);
if (observer instanceof LifecycleObserver) {
mObservers.remove(observer);
}
}
/**
* Pass all onAttach event to {@link LifecycleObserver}.
*/
public void onAttach(Context context) {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnAttach) {
((OnAttach) observer).onAttach();
}
}
}
// This method is not called from the proxy because it does not have access to the
// savedInstanceState
public void onCreate(Bundle savedInstanceState) {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnCreate) {
((OnCreate) observer).onCreate(savedInstanceState);
}
}
}
private void onStart() {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnStart) {
((OnStart) observer).onStart();
}
}
}
public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof SetPreferenceScreen) {
((SetPreferenceScreen) observer).setPreferenceScreen(preferenceScreen);
}
}
}
private void onResume() {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnResume) {
((OnResume) observer).onResume();
}
}
}
private void onPause() {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnPause) {
((OnPause) observer).onPause();
}
}
}
public void onSaveInstanceState(Bundle outState) {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnSaveInstanceState) {
((OnSaveInstanceState) observer).onSaveInstanceState(outState);
}
}
}
private void onStop() {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnStop) {
((OnStop) observer).onStop();
}
}
}
private void onDestroy() {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnDestroy) {
((OnDestroy) observer).onDestroy();
}
}
}
public void onCreateOptionsMenu(final Menu menu, final @Nullable MenuInflater inflater) {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnCreateOptionsMenu) {
((OnCreateOptionsMenu) observer).onCreateOptionsMenu(menu, inflater);
}
}
}
public void onPrepareOptionsMenu(final Menu menu) {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnPrepareOptionsMenu) {
((OnPrepareOptionsMenu) observer).onPrepareOptionsMenu(menu);
}
}
}
public boolean onOptionsItemSelected(final MenuItem menuItem) {
for (int i = 0, size = mObservers.size(); i < size; i++) {
final LifecycleObserver observer = mObservers.get(i);
if (observer instanceof OnOptionsItemSelected) {
if (((OnOptionsItemSelected) observer).onOptionsItemSelected(menuItem)) {
return true;
}
}
}
return false;
}
private class LifecycleProxy
implements androidx.lifecycle.LifecycleObserver {
@OnLifecycleEvent(ON_ANY)
public void onLifecycleEvent(LifecycleOwner owner, Event event) {
switch (event) {
case ON_CREATE:
// onCreate is called directly since we don't have savedInstanceState here
break;
case ON_START:
onStart();
break;
case ON_RESUME:
onResume();
break;
case ON_PAUSE:
onPause();
break;
case ON_STOP:
onStop();
break;
case ON_DESTROY:
onDestroy();
break;
case ON_ANY:
Log.wtf(TAG, "Should not receive an 'ANY' event!");
break;
}
}
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2016 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.settingslib.core.lifecycle;
/**
* Observer of lifecycle events.
* @deprecated use {@link androidx.lifecycle.LifecycleObserver} instead
*/
@Deprecated
public interface LifecycleObserver extends
androidx.lifecycle.LifecycleObserver {
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright (C) 2016 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.settingslib.core.lifecycle;
import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
import static androidx.lifecycle.Lifecycle.Event.ON_START;
import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
import android.app.Activity;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.view.Menu;
import android.view.MenuItem;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LifecycleOwner;
/**
* {@link Activity} that has hooks to observe activity lifecycle events.
*/
public class ObservableActivity extends FragmentActivity implements LifecycleOwner {
private final Lifecycle mLifecycle = new Lifecycle(this);
public Lifecycle getSettingsLifecycle() {
return mLifecycle;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mLifecycle.onAttach(this);
mLifecycle.onCreate(savedInstanceState);
mLifecycle.handleLifecycleEvent(ON_CREATE);
super.onCreate(savedInstanceState);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState,
@Nullable PersistableBundle persistentState) {
mLifecycle.onAttach(this);
mLifecycle.onCreate(savedInstanceState);
mLifecycle.handleLifecycleEvent(ON_CREATE);
super.onCreate(savedInstanceState, persistentState);
}
@Override
protected void onStart() {
mLifecycle.handleLifecycleEvent(ON_START);
super.onStart();
}
@Override
protected void onResume() {
mLifecycle.handleLifecycleEvent(ON_RESUME);
super.onResume();
}
@Override
protected void onPause() {
mLifecycle.handleLifecycleEvent(ON_PAUSE);
super.onPause();
}
@Override
protected void onStop() {
mLifecycle.handleLifecycleEvent(ON_STOP);
super.onStop();
}
@Override
protected void onDestroy() {
mLifecycle.handleLifecycleEvent(ON_DESTROY);
super.onDestroy();
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
if (super.onCreateOptionsMenu(menu)) {
mLifecycle.onCreateOptionsMenu(menu, null);
return true;
}
return false;
}
@Override
public boolean onPrepareOptionsMenu(final Menu menu) {
if (super.onPrepareOptionsMenu(menu)) {
mLifecycle.onPrepareOptionsMenu(menu);
return true;
}
return false;
}
@Override
public boolean onOptionsItemSelected(final MenuItem menuItem) {
boolean lifecycleHandled = mLifecycle.onOptionsItemSelected(menuItem);
if (!lifecycleHandled) {
return super.onOptionsItemSelected(menuItem);
}
return lifecycleHandled;
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2016 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.settingslib.core.lifecycle;
import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
import static androidx.lifecycle.Lifecycle.Event.ON_DESTROY;
import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
import static androidx.lifecycle.Lifecycle.Event.ON_START;
import static androidx.lifecycle.Lifecycle.Event.ON_STOP;
import android.content.Context;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.LifecycleOwner;
/**
* {@link DialogFragment} that has hooks to observe fragment lifecycle events.
*/
public class ObservableDialogFragment extends DialogFragment implements LifecycleOwner {
protected final Lifecycle mLifecycle = new Lifecycle(this);
public Lifecycle getSettingsLifecycle() {
return mLifecycle;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mLifecycle.onAttach(context);
}
@Override
public void onCreate(Bundle savedInstanceState) {
mLifecycle.onCreate(savedInstanceState);
mLifecycle.handleLifecycleEvent(ON_CREATE);
super.onCreate(savedInstanceState);
}
@Override
public void onStart() {
mLifecycle.handleLifecycleEvent(ON_START);
super.onStart();
}
@Override
public void onResume() {
mLifecycle.handleLifecycleEvent(ON_RESUME);
super.onResume();
}
@Override
public void onPause() {
mLifecycle.handleLifecycleEvent(ON_PAUSE);
super.onPause();
}
@Override
public void onStop() {
mLifecycle.handleLifecycleEvent(ON_STOP);
super.onStop();
}
@Override
public void onDestroy() {
mLifecycle.handleLifecycleEvent(ON_DESTROY);
super.onDestroy();
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
mLifecycle.onCreateOptionsMenu(menu, inflater);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public void onPrepareOptionsMenu(final Menu menu) {
mLifecycle.onPrepareOptionsMenu(menu);
super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(final MenuItem menuItem) {
boolean lifecycleHandled = mLifecycle.onOptionsItemSelected(menuItem);
if (!lifecycleHandled) {
return super.onOptionsItemSelected(menuItem);
}
return lifecycleHandled;
}
}

Some files were not shown because too many files have changed in this diff Show More