fix: 首次提交
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
85
SettingsLib/src/com/android/settingslib/AppItem.java
Normal file
85
SettingsLib/src/com/android/settingslib/AppItem.java
Normal 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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
222
SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java
Normal file
222
SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
208
SettingsLib/src/com/android/settingslib/NetworkPolicyEditor.java
Normal file
208
SettingsLib/src/com/android/settingslib/NetworkPolicyEditor.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
101
SettingsLib/src/com/android/settingslib/SignalIcon.java
Normal file
101
SettingsLib/src/com/android/settingslib/SignalIcon.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
34
SettingsLib/src/com/android/settingslib/TetherUtil.java
Normal file
34
SettingsLib/src/com/android/settingslib/TetherUtil.java
Normal 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;
|
||||
}
|
||||
}
|
||||
33
SettingsLib/src/com/android/settingslib/TronUtils.java
Normal file
33
SettingsLib/src/com/android/settingslib/TronUtils.java
Normal 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);
|
||||
}
|
||||
}
|
||||
869
SettingsLib/src/com/android/settingslib/Utils.java
Normal file
869
SettingsLib/src/com/android/settingslib/Utils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
37
SettingsLib/src/com/android/settingslib/WirelessUtils.java
Normal file
37
SettingsLib/src/com/android/settingslib/WirelessUtils.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;";
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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!!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, "");
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
SettingsLib/src/com/android/settingslib/bluetooth/OWNERS
Normal file
12
SettingsLib/src/com/android/settingslib/bluetooth/OWNERS
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user