diff options
author | Xin Li <delphij@google.com> | 2020-09-10 17:22:01 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2020-09-10 17:22:01 +0000 |
commit | 8ac6741e47c76bde065f868ea64d2f04541487b9 (patch) | |
tree | 1a679458fdbd8d370692d56791e2bf83acee35b5 /packages/SettingsLib/src | |
parent | 3de940cc40b1e3fdf8224e18a8308a16768cbfa8 (diff) | |
parent | c64112eb974e9aa7638aead998f07a868acfb5a7 (diff) |
Merge "Merge Android R"
Diffstat (limited to 'packages/SettingsLib/src')
62 files changed, 3513 insertions, 1252 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/NetworkPolicyEditor.java b/packages/SettingsLib/src/com/android/settingslib/NetworkPolicyEditor.java index b01fc8541957..f5aa652f3194 100644 --- a/packages/SettingsLib/src/com/android/settingslib/NetworkPolicyEditor.java +++ b/packages/SettingsLib/src/com/android/settingslib/NetworkPolicyEditor.java @@ -216,7 +216,7 @@ public class NetworkPolicyEditor { private static NetworkTemplate buildUnquotedNetworkTemplate(NetworkTemplate template) { if (template == null) return null; final String networkId = template.getNetworkId(); - final String strippedNetworkId = WifiInfo.removeDoubleQuotes(networkId); + final String strippedNetworkId = WifiInfo.sanitizeSsid(networkId); if (!TextUtils.equals(strippedNetworkId, networkId)) { return new NetworkTemplate( template.getMatchRule(), template.getSubscriberId(), strippedNetworkId); diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java index 9a41f1d6a2b1..9f16d033aea5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java @@ -408,23 +408,6 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils { } /** - * Checks if {@link android.app.admin.DevicePolicyManager#setAutoTimeRequired} is enforced - * on the device. - * - * @return EnforcedAdmin Object containing the device owner component and - * userId the device owner is running as, or {@code null} setAutoTimeRequired is not enforced. - */ - public static EnforcedAdmin checkIfAutoTimeRequired(Context context) { - DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService( - Context.DEVICE_POLICY_SERVICE); - if (dpm == null || !dpm.getAutoTimeRequired()) { - return null; - } - ComponentName adminComponent = dpm.getDeviceOwnerComponentOnCallingUser(); - return new EnforcedAdmin(adminComponent, getUserHandleOf(UserHandle.myUserId())); - } - - /** * Checks if an admin has enforced minimum password quality requirements on the given user. * * @return EnforcedAdmin Object containing the enforced admin component and admin user details, @@ -663,7 +646,8 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils { final SpannableStringBuilder sb = new SpannableStringBuilder(textView.getText()); removeExistingRestrictedSpans(sb); if (disabled) { - final int disabledColor = context.getColor(R.color.disabled_text_color); + 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); diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java index 0ed507c46372..5c05a1bd6722 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java @@ -24,6 +24,8 @@ 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.core.content.res.TypedArrayUtils; @@ -39,6 +41,7 @@ public class RestrictedSwitchPreference extends SwitchPreference { RestrictedPreferenceHelper mHelper; boolean mUseAdditionalSummary = false; CharSequence mRestrictedSwitchSummary; + private int mIconSize; public RestrictedSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { @@ -62,7 +65,7 @@ public class RestrictedSwitchPreference extends SwitchPreference { && restrictedSwitchSummary.type == TypedValue.TYPE_STRING) { if (restrictedSwitchSummary.resourceId != 0) { mRestrictedSwitchSummary = - context.getText(restrictedSwitchSummary.resourceId); + context.getText(restrictedSwitchSummary.resourceId); } else { mRestrictedSwitchSummary = restrictedSwitchSummary.string; } @@ -87,6 +90,10 @@ public class RestrictedSwitchPreference extends SwitchPreference { this(context, null); } + public void setIconSize(int iconSize) { + mIconSize = iconSize; + } + @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); @@ -95,7 +102,7 @@ public class RestrictedSwitchPreference extends SwitchPreference { CharSequence switchSummary; if (mRestrictedSwitchSummary == null) { switchSummary = getContext().getText(isChecked() - ? R.string.enabled_by_admin : R.string.disabled_by_admin); + ? R.string.enabled_by_admin : R.string.disabled_by_admin); } else { switchSummary = mRestrictedSwitchSummary; } @@ -109,6 +116,12 @@ public class RestrictedSwitchPreference extends SwitchPreference { switchWidget.setVisibility(isDisabledByAdmin() ? View.GONE : View.VISIBLE); } + 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); diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java index 1141daa94e6d..a43412e116c8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/Utils.java +++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java @@ -3,6 +3,7 @@ package com.android.settingslib; import android.annotation.ColorInt; 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; @@ -13,6 +14,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Color; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.location.LocationManager; import android.media.AudioManager; @@ -29,14 +31,14 @@ import android.telephony.ServiceState; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.UserIcons; +import com.android.launcher3.icons.IconFactory; import com.android.settingslib.drawable.UserIconDrawable; +import com.android.settingslib.fuelgauge.BatteryStatus; import java.text.NumberFormat; public class Utils { - private static final String CURRENT_MODE_KEY = "CURRENT_MODE"; - private static final String NEW_MODE_KEY = "NEW_MODE"; @VisibleForTesting static final String STORAGE_MANAGER_ENABLED_PROPERTY = "ro.storage_manager.enabled"; @@ -56,24 +58,11 @@ public class Utils { public static void updateLocationEnabled(Context context, boolean enabled, int userId, int source) { - LocationManager locationManager = context.getSystemService(LocationManager.class); - Settings.Secure.putIntForUser( context.getContentResolver(), Settings.Secure.LOCATION_CHANGER, source, userId); - Intent intent = new Intent(LocationManager.MODE_CHANGING_ACTION); - final int oldMode = locationManager.isLocationEnabled() - ? Settings.Secure.LOCATION_MODE_ON - : Settings.Secure.LOCATION_MODE_OFF; - final int newMode = enabled - ? Settings.Secure.LOCATION_MODE_ON - : Settings.Secure.LOCATION_MODE_OFF; - intent.putExtra(CURRENT_MODE_KEY, oldMode); - intent.putExtra(NEW_MODE_KEY, newMode); - context.sendBroadcastAsUser( - intent, UserHandle.of(userId), android.Manifest.permission.WRITE_SECURE_SETTINGS); - + LocationManager locationManager = context.getSystemService(LocationManager.class); locationManager.setLocationEnabledForUser(enabled, UserHandle.of(userId)); } @@ -132,7 +121,7 @@ public class Utils { public static Drawable getUserIcon(Context context, UserManager um, UserInfo user) { final int iconSize = UserIconDrawable.getSizeForList(context); if (user.isManagedProfile()) { - Drawable drawable = UserIconDrawable.getManagedUserDrawable(context); + Drawable drawable = UserIconDrawable.getManagedUserDrawable(context); drawable.setBounds(0, 0, iconSize, iconSize); return drawable; } @@ -174,20 +163,43 @@ public class Utils { return (level * 100) / scale; } - public static String getBatteryStatus(Resources res, Intent batteryChangedIntent) { - int status = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_STATUS, + /** + * Get battery status string + * + * @param context the context + * @param batteryChangedIntent battery broadcast intent received from {@link + * Intent.ACTION_BATTERY_CHANGED}. + * @return battery status string + */ + public static String getBatteryStatus(Context context, Intent batteryChangedIntent) { + final int status = batteryChangedIntent.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN); - String statusString; - if (status == BatteryManager.BATTERY_STATUS_CHARGING) { - statusString = res.getString(R.string.battery_info_status_charging); - } 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); - } else if (status == BatteryManager.BATTERY_STATUS_FULL) { + 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(R.string.battery_info_status_full); } else { - statusString = res.getString(R.string.battery_info_status_unknown); + if (status == BatteryManager.BATTERY_STATUS_CHARGING) { + 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 (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; @@ -218,6 +230,13 @@ public class Utils { 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); @@ -258,8 +277,12 @@ public class Utils { } 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, 0); + int theme = ta.getResourceId(0, defaultValue); ta.recycle(); return theme; } @@ -432,6 +455,21 @@ public class Utils { return state; } + /** Get the corresponding adaptive icon drawable. */ + public static Drawable getBadgedIcon(Context context, Drawable icon, UserHandle user) { + try (IconFactory iconFactory = IconFactory.obtain(context)) { + final Bitmap iconBmp = iconFactory.createBadgedIconBitmap(icon, user, + true /* shrinkNonAdaptiveIcons */).icon; + return new BitmapDrawable(context.getResources(), iconBmp); + } + } + + /** 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)); + } + private static boolean isNotInIwlan(ServiceState serviceState) { final NetworkRegistrationInfo networkRegWlan = serviceState.getNetworkRegistrationInfo( NetworkRegistrationInfo.DOMAIN_PS, diff --git a/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java b/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java index a18600abf788..59735f413f9d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java @@ -48,11 +48,21 @@ public class AccessibilityUtils { 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> enabledServiceInfos) { - for (int i = 0; i < enabledServiceInfos.size(); i++) { - AccessibilityServiceInfo accessibilityServiceInfo = enabledServiceInfos.get(i); - final ServiceInfo serviceInfo = enabledServiceInfos.get(i).getResolveInfo().serviceInfo; + 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; @@ -179,19 +189,6 @@ public class AccessibilityUtils { return context.getString(R.string.config_defaultAccessibilityService); } - /** - * Check if the accessibility shortcut is enabled for a user - * - * @param context A valid context - * @param userId The user of interest - * @return {@code true} if the shortcut is enabled for the user. {@code false} otherwise. - * Note that the shortcut may be enabled, but no action associated with it. - */ - public static boolean isShortcutEnabled(Context context, int userId) { - return Settings.Secure.getIntForUser(context.getContentResolver(), - Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 1, userId) == 1; - } - private static Set<ComponentName> getInstalledServices(Context context) { final Set<ComponentName> installedServices = new HashSet<>(); installedServices.clear(); diff --git a/packages/SettingsLib/src/com/android/settingslib/accounts/AuthenticatorHelper.java b/packages/SettingsLib/src/com/android/settingslib/accounts/AuthenticatorHelper.java index ef511bbc8133..4af9e3c441de 100644 --- a/packages/SettingsLib/src/com/android/settingslib/accounts/AuthenticatorHelper.java +++ b/packages/SettingsLib/src/com/android/settingslib/accounts/AuthenticatorHelper.java @@ -32,6 +32,8 @@ 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; @@ -116,7 +118,7 @@ final public class AuthenticatorHelper extends BroadcastReceiver { if (icon == null) { icon = context.getPackageManager().getDefaultActivityIcon(); } - return icon; + return Utils.getBadgedIcon(mContext, icon, mUserHandle); } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java index c4ff71940d20..898796828131 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java @@ -19,10 +19,15 @@ 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.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; 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; @@ -44,6 +49,15 @@ public class AppUtils { */ 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; @@ -111,17 +125,8 @@ public class AppUtils { /** Returns the label for a given package. */ public static CharSequence getApplicationLabel( PackageManager packageManager, String packageName) { - try { - final ApplicationInfo appInfo = - packageManager.getApplicationInfo( - packageName, - PackageManager.MATCH_DISABLED_COMPONENTS - | PackageManager.MATCH_ANY_USER); - return appInfo.loadLabel(packageManager); - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Unable to find info for package: " + packageName); - } - return null; + return com.android.settingslib.utils.applications.AppUtils + .getApplicationLabel(packageManager, packageName); } /** @@ -129,7 +134,7 @@ public class AppUtils { */ public static boolean isHiddenSystemModule(Context context, String packageName) { return ApplicationsState.getInstance((Application) context.getApplicationContext()) - .isHiddenModule(packageName); + .isHiddenModule(packageName); } /** @@ -140,4 +145,57 @@ public class AppUtils { .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 */); + // 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; + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index f9df5a3eef28..a89cf37e2d06 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -59,6 +59,8 @@ import androidx.lifecycle.OnLifecycleEvent; import com.android.internal.R; import com.android.internal.util.ArrayUtils; +import com.android.settingslib.Utils; +import com.android.settingslib.utils.ThreadUtils; import java.io.File; import java.io.IOException; @@ -199,8 +201,7 @@ public class ApplicationsState { mEntriesMap.put(userId, new HashMap<>()); } - mThread = new HandlerThread("ApplicationsState.Loader", - Process.THREAD_PRIORITY_BACKGROUND); + mThread = new HandlerThread("ApplicationsState.Loader"); mThread.start(); mBackgroundHandler = new BackgroundHandler(mThread.getLooper()); @@ -496,7 +497,21 @@ public class ApplicationsState { return; } synchronized (entry) { - entry.ensureIconLocked(mContext, mDrawableFactory); + entry.ensureIconLocked(mContext); + } + } + + /** + * To generate and cache the label description. + * + * @param entry contain the entries of an app + */ + public void ensureLabelDescription(AppEntry entry) { + if (entry.labelDescription != null) { + return; + } + synchronized (entry) { + entry.ensureLabelDescriptionLocked(mContext); } } @@ -866,6 +881,10 @@ public class ApplicationsState { void handleRebuildList() { AppFilter filter; Comparator<AppEntry> comparator; + + if (!mResumed) { + return; + } synchronized (mRebuildSync) { if (!mRebuildRequested) { return; @@ -1070,8 +1089,8 @@ public class ApplicationsState { } } if (rebuildingSessions != null) { - for (int i = 0; i < rebuildingSessions.size(); i++) { - rebuildingSessions.get(i).handleRebuildList(); + for (Session session : rebuildingSessions) { + session.handleRebuildList(); } } @@ -1213,7 +1232,7 @@ public class ApplicationsState { AppEntry entry = mAppEntries.get(i); if (entry.icon == null || !entry.mounted) { synchronized (entry) { - if (entry.ensureIconLocked(mContext, mDrawableFactory)) { + if (entry.ensureIconLocked(mContext)) { if (!mRunning) { mRunning = true; Message m = mMainHandler.obtainMessage( @@ -1520,6 +1539,7 @@ public class ApplicationsState { public long size; public long internalSize; public long externalSize; + public String labelDescription; public boolean mounted; @@ -1569,6 +1589,15 @@ public class ApplicationsState { this.size = SIZE_UNKNOWN; this.sizeStale = true; ensureLabel(context); + // Speed up the cache of the icon and label description if they haven't been created. + ThreadUtils.postOnBackgroundThread(() -> { + if (this.icon == null) { + this.ensureIconLocked(context); + } + if (this.labelDescription == null) { + this.ensureLabelDescriptionLocked(context); + } + }); } public void ensureLabel(Context context) { @@ -1584,10 +1613,10 @@ public class ApplicationsState { } } - boolean ensureIconLocked(Context context, IconDrawableFactory drawableFactory) { + boolean ensureIconLocked(Context context) { if (this.icon == null) { if (this.apkFile.exists()) { - this.icon = drawableFactory.getBadgedIcon(info); + this.icon = Utils.getBadgedIcon(context, info); return true; } else { this.mounted = false; @@ -1598,7 +1627,7 @@ public class ApplicationsState { // its icon. if (this.apkFile.exists()) { this.mounted = true; - this.icon = drawableFactory.getBadgedIcon(info); + this.icon = Utils.getBadgedIcon(context, info); return true; } } @@ -1612,6 +1641,24 @@ public class ApplicationsState { return ""; } } + + /** + * Get the label description 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 label. + * + * @param context The application context + */ + public void ensureLabelDescriptionLocked(Context context) { + final int userId = UserHandle.getUserId(this.info.uid); + if (UserManager.get(context).isManagedProfile(userId)) { + this.labelDescription = context.getString( + com.android.settingslib.R.string.accessibility_work_profile_app_description, + this.label); + } else { + this.labelDescription = this.label; + } + } } private static boolean hasFlag(int flags, int flag) { diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java b/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java index 454d1dce0b2f..bd9e760acfda 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java @@ -47,6 +47,7 @@ public class ServiceListing { 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<>(); @@ -54,7 +55,8 @@ public class ServiceListing { private boolean mListening; private ServiceListing(Context context, String tag, - String setting, String intentAction, String permission, String noun) { + String setting, String intentAction, String permission, String noun, + boolean addDeviceLockedFlags) { mContentResolver = context.getContentResolver(); mContext = context; mTag = tag; @@ -62,6 +64,7 @@ public class ServiceListing { mIntentAction = intentAction; mPermission = permission; mNoun = noun; + mAddDeviceLockedFlags = addDeviceLockedFlags; } public void addCallback(Callback callback) { @@ -125,11 +128,15 @@ public class ServiceListing { 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), - PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, - user); + new Intent(mIntentAction), flags, user); for (ResolveInfo resolveInfo : installedServices) { ServiceInfo info = resolveInfo.serviceInfo; @@ -186,6 +193,7 @@ public class ServiceListing { private String mIntentAction; private String mPermission; private String mNoun; + private boolean mAddDeviceLockedFlags = false; public Builder(Context context) { mContext = context; @@ -216,8 +224,19 @@ public class ServiceListing { 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); + return new ServiceListing(mContext, mTag, mSetting, mIntentAction, mPermission, mNoun, + mAddDeviceLockedFlags); } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java index 29015c9a9f68..59d8acb82196 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java @@ -212,15 +212,21 @@ public class BluetoothEventManager { } private void dispatchAudioModeChanged() { - mDeviceManager.dispatchAudioModeChanged(); + for (CachedBluetoothDevice cachedDevice : mDeviceManager.getCachedDevicesCopy()) { + cachedDevice.onAudioModeChanged(); + } for (BluetoothCallback callback : mCallbacks) { callback.onAudioModeChanged(); } } - private void dispatchActiveDeviceChanged(CachedBluetoothDevice activeDevice, + @VisibleForTesting + void dispatchActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) { - mDeviceManager.onActiveDeviceChanged(activeDevice, bluetoothProfile); + for (CachedBluetoothDevice cachedDevice : mDeviceManager.getCachedDevicesCopy()) { + boolean isActive = Objects.equals(cachedDevice, activeDevice); + cachedDevice.onActiveDeviceChanged(isActive, bluetoothProfile); + } for (BluetoothCallback callback : mCallbacks) { callback.onActiveDeviceChanged(activeDevice, bluetoothProfile); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 2f34b2bad0ad..95e916b9871a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -1,5 +1,7 @@ package com.android.settingslib.bluetooth; +import static com.android.settingslib.widget.AdaptiveOutlineDrawable.AdaptiveOutlineIconType.TYPE_ADVANCED; + import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; @@ -7,6 +9,8 @@ import android.content.Context; import android.content.Intent; 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.net.Uri; import android.provider.MediaStore; @@ -132,6 +136,44 @@ public class BluetoothUtils { */ 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); + } + + return new Pair<>(buildBtRainbowDrawable(context, + pair.first, cachedDevice.getAddress().hashCode()), pair.second); + } + + /** + * Build Bluetooth device icon with rainbow + */ + public 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(); @@ -150,7 +192,7 @@ public class BluetoothUtils { context.getContentResolver().takePersistableUriPermission(iconUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); } catch (SecurityException e) { - Log.e(TAG, "Failed to take persistable permission for: " + iconUri); + Log.e(TAG, "Failed to take persistable permission for: " + iconUri, e); } try { final Bitmap bitmap = MediaStore.Images.Media.getBitmap( @@ -159,38 +201,58 @@ public class BluetoothUtils { final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, false); bitmap.recycle(); - final AdaptiveOutlineDrawable drawable = new AdaptiveOutlineDrawable( - resources, resizedBitmap); - return new Pair<>(drawable, pair.second); + 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<>(buildBtRainbowDrawable(context, - pair.first, cachedDevice.getAddress().hashCode()), pair.second); + return new Pair<>(pair.first, pair.second); } /** - * Build Bluetooth device icon with rainbow + * Build device icon with advanced outline */ - public static Drawable buildBtRainbowDrawable(Context context, Drawable drawable, - int hashCode) { + public static Drawable buildAdvancedDrawable(Context context, Drawable drawable) { + final int iconSize = context.getResources().getDimensionPixelSize( + R.dimen.advanced_icon_size); 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); + 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); + } - // 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]); + if (bitmap != null) { + final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize, + iconSize, false); + bitmap.recycle(); + return new AdaptiveOutlineDrawable(resources, resizedBitmap, TYPE_ADVANCED); + } - return adaptiveIcon; + 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; } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index f694f0e2a83d..4c80b91f300d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -24,6 +24,9 @@ import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.content.Context; import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; import android.os.ParcelUuid; import android.os.SystemClock; import android.text.TextUtils; @@ -38,7 +41,6 @@ import com.android.settingslib.Utils; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -56,6 +58,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> // Some Hearing Aids (especially the 2nd device) needs more time to do service discovery private static final long MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT = 15000; private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000; + private static final long MAX_MEDIA_PROFILE_CONNECT_DELAY = 60000; private final Context mContext; private final BluetoothAdapter mLocalAdapter; @@ -67,10 +70,10 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> short mRssi; // mProfiles and mRemovedProfiles does not do swap() between main and sub device. It is // because current sub device is only for HearingAid and its profile is the same. - private final List<LocalBluetoothProfile> mProfiles = new ArrayList<>(); + private final Collection<LocalBluetoothProfile> mProfiles = new CopyOnWriteArrayList<>(); // List of profiles that were previously in mProfiles, but have been removed - private final List<LocalBluetoothProfile> mRemovedProfiles = new ArrayList<>(); + private final Collection<LocalBluetoothProfile> mRemovedProfiles = new CopyOnWriteArrayList<>(); // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP private boolean mLocalNapRoleConnected; @@ -91,9 +94,35 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> private boolean mIsActiveDeviceA2dp = false; private boolean mIsActiveDeviceHeadset = false; private boolean mIsActiveDeviceHearingAid = false; + // Media profile connect state + private boolean mIsA2dpProfileConnectedFail = false; + private boolean mIsHeadsetProfileConnectedFail = false; + private boolean mIsHearingAidProfileConnectedFail = false; // Group second device for Hearing Aid private CachedBluetoothDevice mSubDevice; + private final Handler mHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case BluetoothProfile.A2DP: + mIsA2dpProfileConnectedFail = true; + break; + case BluetoothProfile.HEADSET: + mIsHeadsetProfileConnectedFail = true; + break; + case BluetoothProfile.HEARING_AID: + mIsHearingAidProfileConnectedFail = true; + break; + default: + Log.w(TAG, "handleMessage(): unknown message : " + msg.what); + break; + } + Log.w(TAG, "Connect to profile : " + msg.what + " timeout, show error message !"); + refresh(); + } + }; + CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager, BluetoothDevice device) { mContext = context; @@ -134,6 +163,35 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } synchronized (mProfileLock) { + if (profile instanceof A2dpProfile || profile instanceof HeadsetProfile + || profile instanceof HearingAidProfile) { + setProfileConnectedStatus(profile.getProfileId(), false); + switch (newProfileState) { + case BluetoothProfile.STATE_CONNECTED: + mHandler.removeMessages(profile.getProfileId()); + break; + case BluetoothProfile.STATE_CONNECTING: + mHandler.sendEmptyMessageDelayed(profile.getProfileId(), + MAX_MEDIA_PROFILE_CONNECT_DELAY); + break; + case BluetoothProfile.STATE_DISCONNECTING: + if (mHandler.hasMessages(profile.getProfileId())) { + mHandler.removeMessages(profile.getProfileId()); + } + break; + case BluetoothProfile.STATE_DISCONNECTED: + if (mHandler.hasMessages(profile.getProfileId())) { + mHandler.removeMessages(profile.getProfileId()); + setProfileConnectedStatus(profile.getProfileId(), true); + } + break; + default: + Log.w(TAG, "onProfileStateChanged(): unknown profile state : " + + newProfileState); + break; + } + } + if (newProfileState == BluetoothProfile.STATE_CONNECTED) { if (profile instanceof MapProfile) { profile.setEnabled(mDevice, true); @@ -163,6 +221,24 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> fetchActiveDevices(); } + @VisibleForTesting + void setProfileConnectedStatus(int profileId, boolean isFailed) { + switch (profileId) { + case BluetoothProfile.A2DP: + mIsA2dpProfileConnectedFail = isFailed; + break; + case BluetoothProfile.HEADSET: + mIsHeadsetProfileConnectedFail = isFailed; + break; + case BluetoothProfile.HEARING_AID: + mIsHearingAidProfileConnectedFail = isFailed; + break; + default: + Log.w(TAG, "setProfileConnectedStatus(): unknown profile id : " + profileId); + break; + } + } + public void disconnect() { synchronized (mProfileLock) { mLocalAdapter.disconnectAllEnabledProfiles(mDevice); @@ -639,10 +715,6 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } public List<LocalBluetoothProfile> getProfiles() { - return Collections.unmodifiableList(mProfiles); - } - - public List<LocalBluetoothProfile> getProfileListCopy() { return new ArrayList<>(mProfiles); } @@ -660,7 +732,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } public List<LocalBluetoothProfile> getRemovedProfiles() { - return mRemovedProfiles; + return new ArrayList<>(mRemovedProfiles); } public void registerCallback(Callback callback) { @@ -828,6 +900,10 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> int leftBattery = -1; int rightBattery = -1; + if (isProfileConnectedFail() && isConnected()) { + return mContext.getString(R.string.profile_connect_timeout_subtext); + } + synchronized (mProfileLock) { for (LocalBluetoothProfile profile : getProfiles()) { int connectionStatus = getProfileConnectionState(profile); @@ -927,6 +1003,11 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> return leftBattery >= 0 && rightBattery >= 0; } + private boolean isProfileConnectedFail() { + return mIsA2dpProfileConnectedFail || mIsHearingAidProfileConnectedFail + || mIsHeadsetProfileConnectedFail; + } + /** * @return resource for android auto string that describes the connection state of this device. */ diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index 7050db14bfb1..cca9cfac2d22 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -26,7 +26,6 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Objects; /** * CachedBluetoothDeviceManager manages the set of remote Bluetooth devices. @@ -97,14 +96,17 @@ public class CachedBluetoothDeviceManager { * @return the newly created CachedBluetoothDevice object */ public CachedBluetoothDevice addDevice(BluetoothDevice device) { - LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); - CachedBluetoothDevice newDevice = new CachedBluetoothDevice(mContext, profileManager, - device); - mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(newDevice); + CachedBluetoothDevice newDevice; + final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); synchronized (this) { - if (!mHearingAidDeviceManager.setSubDeviceIfNeeded(newDevice)) { - mCachedDevices.add(newDevice); - mBtManager.getEventManager().dispatchDeviceAdded(newDevice); + newDevice = findDevice(device); + if (newDevice == null) { + newDevice = new CachedBluetoothDevice(mContext, profileManager, device); + mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(newDevice); + if (!mHearingAidDeviceManager.setSubDeviceIfNeeded(newDevice)) { + mCachedDevices.add(newDevice); + mBtManager.getEventManager().dispatchDeviceAdded(newDevice); + } } } @@ -226,14 +228,6 @@ public class CachedBluetoothDeviceManager { } } - public synchronized void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, - int bluetoothProfile) { - for (CachedBluetoothDevice cachedDevice : mCachedDevices) { - boolean isActive = Objects.equals(cachedDevice, activeDevice); - cachedDevice.onActiveDeviceChanged(isActive, bluetoothProfile); - } - } - public synchronized boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, int state) { return mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice, @@ -254,12 +248,6 @@ public class CachedBluetoothDeviceManager { } } - public synchronized void dispatchAudioModeChanged() { - for (CachedBluetoothDevice cachedDevice : mCachedDevices) { - cachedDevice.onAudioModeChanged(); - } - } - private void log(String msg) { if (DEBUG) { Log.d(TAG, msg); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java index c72efb7eec83..35bbbc0e8b39 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java @@ -48,6 +48,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; /** @@ -232,7 +233,7 @@ public class LocalBluetoothProfileManager { } private final Collection<ServiceListener> mServiceListeners = - new ArrayList<ServiceListener>(); + new CopyOnWriteArrayList<ServiceListener>(); private void addProfile(LocalBluetoothProfile profile, String profileName, String stateChangedAction) { @@ -361,14 +362,18 @@ public class LocalBluetoothProfileManager { // not synchronized: use only from UI thread! (TODO: verify) void callServiceConnectedListeners() { - for (ServiceListener l : mServiceListeners) { + final Collection<ServiceListener> listeners = new ArrayList<>(mServiceListeners); + + for (ServiceListener l : listeners) { l.onServiceConnected(); } } // not synchronized: use only from UI thread! (TODO: verify) void callServiceDisconnectedListeners() { - for (ServiceListener listener : mServiceListeners) { + final Collection<ServiceListener> listeners = new ArrayList<>(mServiceListeners); + + for (ServiceListener listener : listeners) { listener.onServiceDisconnected(); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java index e8245082f8ef..d84e57a38ee4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java @@ -30,16 +30,22 @@ import com.android.internal.logging.nano.MetricsProto; public class EventLogWriter implements LogWriter { @Override - public void visible(Context context, int source, int category) { + 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_CONTEXT, source) + .addTaggedData(MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE, + latency); MetricsLogger.action(logMaker); } @Override - public void hidden(Context context, int category) { - MetricsLogger.hidden(context, category); + 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 diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java index f1876883a336..d4ef3d7b24a2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java @@ -26,12 +26,12 @@ public interface LogWriter { /** * Logs a visibility event when view becomes visible. */ - void visible(Context context, int source, int category); + void visible(Context context, int source, int category, int latency); /** * Logs a visibility event when view becomes hidden. */ - void hidden(Context context, int category); + void hidden(Context context, int category, int visibleTime); /** * Logs an user action. diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java index 8cc3b5a3f37a..bd0b9e93b09d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java @@ -23,6 +23,9 @@ 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; @@ -67,15 +70,28 @@ public class MetricsFeatureProvider { SettingsEnums.PAGE_UNKNOWN); } - public void visible(Context context, int source, int category) { + /** + * 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); + writer.visible(context, source, category, latency); } } - public void hidden(Context context, int category) { + /** + * 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); + writer.hidden(context, category, visibleTime); } } @@ -125,34 +141,65 @@ public class MetricsFeatureProvider { return ((Instrumentable) object).getMetricsCategory(); } - public void logDashboardStartIntent(Context context, Intent intent, - int sourceMetricsCategory) { + /** + * 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; + return false; } final ComponentName cn = intent.getComponent(); - if (cn == null) { - final String action = intent.getAction(); - if (TextUtils.isEmpty(action)) { - // Not loggable - return; - } - action(sourceMetricsCategory, - MetricsEvent.ACTION_SETTINGS_TILE_CLICK, - SettingsEnums.PAGE_UNKNOWN, - action, - 0); - return; - } else if (TextUtils.equals(cn.getPackageName(), context.getPackageName())) { - // Going to a Setting internal page, skip click logging in favor of page's own - // visibility logging. - return; + final String key = cn != null ? cn.flattenToString() : intent.getAction(); + return logSettingsTileClick(key + (isWorkProfile ? "/work" : "/personal"), + sourceMetricsCategory); + } + + /** + * 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; } action(sourceMetricsCategory, MetricsEvent.ACTION_SETTINGS_TILE_CLICK, SettingsEnums.PAGE_UNKNOWN, - cn.flattenToString(), + logKey, 0); + return true; } - } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java index 8090169a4245..6e7a429e6b7a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java @@ -23,15 +23,16 @@ import android.content.Intent; import android.os.SystemClock; import androidx.lifecycle.Lifecycle.Event; -import androidx.lifecycle.LifecycleObserver; 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 { +public class VisibilityLoggerMixin implements LifecycleObserver, OnAttach { private static final String TAG = "VisibilityLoggerMixin"; @@ -39,6 +40,7 @@ public class VisibilityLoggerMixin implements LifecycleObserver { private MetricsFeatureProvider mMetricsFeature; private int mSourceMetricsCategory = MetricsProto.MetricsEvent.VIEW_UNKNOWN; + private long mCreationTimestamp; private long mVisibleTimestamp; public VisibilityLoggerMixin(int metricsCategory, MetricsFeatureProvider metricsFeature) { @@ -46,19 +48,48 @@ public class VisibilityLoggerMixin implements LifecycleObserver { 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 (mMetricsFeature != null && mMetricsCategory != METRICS_CATEGORY_UNKNOWN) { - mMetricsFeature.visible(null /* context */, mSourceMetricsCategory, mMetricsCategory); + 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() { - mVisibleTimestamp = 0; + mCreationTimestamp = 0; if (mMetricsFeature != null && mMetricsCategory != METRICS_CATEGORY_UNKNOWN) { - mMetricsFeature.hidden(null /* context */, mMetricsCategory); + 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); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/Lifecycle.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/Lifecycle.java index 56de280a0049..f87c88695686 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/Lifecycle.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/Lifecycle.java @@ -94,11 +94,14 @@ public class Lifecycle extends LifecycleRegistry { } } + /** + * 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(context); + ((OnAttach) observer).onAttach(); } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnAttach.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnAttach.java index e28c38736639..1e7d01c15fbe 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnAttach.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnAttach.java @@ -15,12 +15,12 @@ */ package com.android.settingslib.core.lifecycle.events; -import android.content.Context; - /** - * @deprecated pass {@link Context} in constructor instead + * An Interface used by {@link LifecycleObserver} which changes to onAttach state. */ -@Deprecated public interface OnAttach { - void onAttach(Context context); + /** + * Called when {@link LifecycleObserver} is entering onAttach + */ + void onAttach(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractImsStatusPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractImsStatusPreferenceController.java index a5f403690dab..d427f7a20dba 100644 --- a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractImsStatusPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractImsStatusPreferenceController.java @@ -23,7 +23,9 @@ import android.net.wifi.WifiManager; import android.os.PersistableBundle; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; -import android.telephony.TelephonyManager; +import android.telephony.ims.ImsMmTelManager; +import android.telephony.ims.RegistrationManager; +import android.util.Log; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; @@ -32,19 +34,30 @@ import androidx.preference.PreferenceScreen; import com.android.settingslib.R; import com.android.settingslib.core.lifecycle.Lifecycle; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + /** * Preference controller for IMS status */ public abstract class AbstractImsStatusPreferenceController extends AbstractConnectivityPreferenceController { + private static final String LOG_TAG = "AbstractImsPrefController"; + @VisibleForTesting static final String KEY_IMS_REGISTRATION_STATE = "ims_reg_state"; + private static final long MAX_THREAD_BLOCKING_TIME_MS = 2000; + private static final String[] CONNECTIVITY_INTENTS = { BluetoothAdapter.ACTION_STATE_CHANGED, ConnectivityManager.CONNECTIVITY_ACTION, - WifiManager.LINK_CONFIGURATION_CHANGED_ACTION, + WifiManager.ACTION_LINK_CONFIGURATION_CHANGED, WifiManager.NETWORK_STATE_CHANGED_ACTION, }; @@ -57,8 +70,9 @@ public abstract class AbstractImsStatusPreferenceController @Override public boolean isAvailable() { - CarrierConfigManager configManager = mContext.getSystemService(CarrierConfigManager.class); - int subId = SubscriptionManager.getDefaultDataSubscriptionId(); + final CarrierConfigManager configManager = + mContext.getSystemService(CarrierConfigManager.class); + final int subId = SubscriptionManager.getDefaultDataSubscriptionId(); PersistableBundle config = null; if (configManager != null) { config = configManager.getConfigForSubId(subId); @@ -86,11 +100,57 @@ public abstract class AbstractImsStatusPreferenceController @Override protected void updateConnectivity() { - int subId = SubscriptionManager.getDefaultDataSubscriptionId(); - if (mImsStatus != null) { - TelephonyManager tm = mContext.getSystemService(TelephonyManager.class); - mImsStatus.setSummary((tm != null && tm.isImsRegistered(subId)) ? - R.string.ims_reg_status_registered : R.string.ims_reg_status_not_registered); + if (mImsStatus == null) { + return; + } + final int subId = SubscriptionManager.getDefaultDataSubscriptionId(); + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + mImsStatus.setSummary(R.string.ims_reg_status_not_registered); + return; + } + final ExecutorService executors = Executors.newSingleThreadExecutor(); + final StateCallback stateCallback = new StateCallback(); + + final ImsMmTelManager imsMmTelManager = ImsMmTelManager.createForSubscriptionId(subId); + try { + imsMmTelManager.getRegistrationState(executors, stateCallback); + } catch (Exception ex) { + } + + mImsStatus.setSummary(stateCallback.waitUntilResult() + ? R.string.ims_reg_status_registered : R.string.ims_reg_status_not_registered); + + try { + executors.shutdownNow(); + } catch (Exception exception) { + } + } + + private final class StateCallback extends AtomicBoolean implements Consumer<Integer> { + private StateCallback() { + super(false); + mSemaphore = new Semaphore(0); + } + + private final Semaphore mSemaphore; + + public void accept(Integer state) { + set(state == RegistrationManager.REGISTRATION_STATE_REGISTERED); + try { + mSemaphore.release(); + } catch (Exception ex) { + } + } + + public boolean waitUntilResult() { + try { + if (!mSemaphore.tryAcquire(MAX_THREAD_BLOCKING_TIME_MS, TimeUnit.MILLISECONDS)) { + Log.w(LOG_TAG, "IMS registration state query timeout"); + return false; + } + } catch (Exception ex) { + } + return get(); } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractIpAddressPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractIpAddressPreferenceController.java index 24da72ea611a..3bb3a0c412a5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractIpAddressPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractIpAddressPreferenceController.java @@ -42,7 +42,7 @@ public abstract class AbstractIpAddressPreferenceController private static final String[] CONNECTIVITY_INTENTS = { ConnectivityManager.CONNECTIVITY_ACTION, - WifiManager.LINK_CONFIGURATION_CHANGED_ACTION, + WifiManager.ACTION_LINK_CONFIGURATION_CHANGED, WifiManager.NETWORK_STATE_CHANGED_ACTION, }; diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java index 71778215e079..b5f275b463f4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java @@ -44,7 +44,7 @@ public abstract class AbstractWifiMacAddressPreferenceController private static final String[] CONNECTIVITY_INTENTS = { ConnectivityManager.CONNECTIVITY_ACTION, - WifiManager.LINK_CONFIGURATION_CHANGED_ACTION, + WifiManager.ACTION_LINK_CONFIGURATION_CHANGED, WifiManager.NETWORK_STATE_CHANGED_ACTION, }; @@ -69,8 +69,10 @@ public abstract class AbstractWifiMacAddressPreferenceController @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - mWifiMacAddress = screen.findPreference(KEY_WIFI_MAC_ADDRESS); - updateConnectivity(); + if (isAvailable()) { + mWifiMacAddress = screen.findPreference(KEY_WIFI_MAC_ADDRESS); + updateConnectivity(); + } } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/display/BrightnessUtils.java b/packages/SettingsLib/src/com/android/settingslib/display/BrightnessUtils.java index 55723f9d8ed4..4f86afaa995c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/display/BrightnessUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/display/BrightnessUtils.java @@ -20,7 +20,8 @@ import android.util.MathUtils; public class BrightnessUtils { - public static final int GAMMA_SPACE_MAX = 1023; + public static final int GAMMA_SPACE_MIN = 0; + public static final int GAMMA_SPACE_MAX = 65535; // Hybrid Log Gamma constant values private static final float R = 0.5f; @@ -51,7 +52,7 @@ public class BrightnessUtils { * @return The corresponding setting value. */ public static final int convertGammaToLinear(int val, int min, int max) { - final float normalizedVal = MathUtils.norm(0, GAMMA_SPACE_MAX, val); + final float normalizedVal = MathUtils.norm(GAMMA_SPACE_MIN, GAMMA_SPACE_MAX, val); final float ret; if (normalizedVal <= R) { ret = MathUtils.sq(normalizedVal / R); @@ -65,6 +66,33 @@ public class BrightnessUtils { } /** + * Version of {@link #convertGammaToLinear} that takes and returns float values. + * TODO(flc): refactor Android Auto to use float version + * + * @param val The slider value. + * @param min The minimum acceptable value for the setting. + * @param max The maximum acceptable value for the setting. + * @return The corresponding setting value. + */ + public static final float convertGammaToLinearFloat(int val, float min, float max) { + final float normalizedVal = MathUtils.norm(GAMMA_SPACE_MIN, GAMMA_SPACE_MAX, val); + final float ret; + if (normalizedVal <= R) { + ret = MathUtils.sq(normalizedVal / R); + } else { + ret = MathUtils.exp((normalizedVal - C) / A) + B; + } + + // HLG is normalized to the range [0, 12], ensure that value is within that range, + // it shouldn't be out of bounds. + final float normalizedRet = MathUtils.constrain(ret, 0, 12); + + // Re-normalize to the range [0, 1] + // in order to derive the correct setting value. + return MathUtils.lerp(min, max, normalizedRet / 12); + } + + /** * A function for converting from the linear space that the setting works in to the * gamma space that the slider works in. * @@ -87,6 +115,18 @@ public class BrightnessUtils { * @return The corresponding slider value */ public static final int convertLinearToGamma(int val, int min, int max) { + return convertLinearToGammaFloat((float) val, (float) min, (float) max); + } + + /** + * Version of {@link #convertLinearToGamma} that takes float values. + * TODO: brightnessfloat merge with above method(?) + * @param val The brightness setting value. + * @param min The minimum acceptable value for the setting. + * @param max The maximum acceptable value for the setting. + * @return The corresponding slider value + */ + public static final int convertLinearToGammaFloat(float val, float min, float max) { // For some reason, HLG normalizes to the range [0, 12] rather than [0, 1] final float normalizedVal = MathUtils.norm(min, max, val) * 12; final float ret; @@ -96,6 +136,6 @@ public class BrightnessUtils { ret = A * MathUtils.log(normalizedVal - B) + C; } - return Math.round(MathUtils.lerp(0, GAMMA_SPACE_MAX, ret)); + return Math.round(MathUtils.lerp(GAMMA_SPACE_MIN, GAMMA_SPACE_MAX, ret)); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java index e0ca1ab0c07c..a38091debb64 100644 --- a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java @@ -97,7 +97,7 @@ public class DisplayDensityUtils { final Resources res = context.getResources(); final DisplayMetrics metrics = new DisplayMetrics(); - context.getDisplay().getRealMetrics(metrics); + context.getDisplayNoVerify().getRealMetrics(metrics); final int currentDensity = metrics.densityDpi; int currentDensityIndex = -1; diff --git a/packages/SettingsLib/src/com/android/settingslib/drawable/CircleFramedDrawable.java b/packages/SettingsLib/src/com/android/settingslib/drawable/CircleFramedDrawable.java index 278b57da0c28..e5ea4467517b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/drawable/CircleFramedDrawable.java +++ b/packages/SettingsLib/src/com/android/settingslib/drawable/CircleFramedDrawable.java @@ -41,7 +41,7 @@ public class CircleFramedDrawable extends Drawable { private final Bitmap mBitmap; private final int mSize; - private final Paint mPaint; + private Paint mIconPaint; private float mScale; private Rect mSrcRect; @@ -75,18 +75,18 @@ public class CircleFramedDrawable extends Drawable { canvas.drawColor(0, PorterDuff.Mode.CLEAR); // opaque circle matte - mPaint = new Paint(); - mPaint.setAntiAlias(true); - mPaint.setColor(Color.BLACK); - mPaint.setStyle(Paint.Style.FILL); - canvas.drawPath(fillPath, mPaint); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setColor(Color.BLACK); + paint.setStyle(Paint.Style.FILL); + canvas.drawPath(fillPath, paint); // mask in the icon where the bitmap is opaque - mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); - canvas.drawBitmap(icon, cropRect, circleRect, mPaint); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(icon, cropRect, circleRect, paint); // prepare paint for frame drawing - mPaint.setXfermode(null); + paint.setXfermode(null); mScale = 1f; @@ -100,7 +100,7 @@ public class CircleFramedDrawable extends Drawable { final float pad = (mSize - inside) / 2f; mDstRect.set(pad, pad, mSize - pad, mSize - pad); - canvas.drawBitmap(mBitmap, mSrcRect, mDstRect, null); + canvas.drawBitmap(mBitmap, mSrcRect, mDstRect, mIconPaint); } public void setScale(float scale) { @@ -122,8 +122,12 @@ public class CircleFramedDrawable extends Drawable { @Override public void setColorFilter(ColorFilter cf) { + if (mIconPaint == null) { + mIconPaint = new Paint(); + } + mIconPaint.setColorFilter(cf); } - + @Override public int getIntrinsicWidth() { return mSize; diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java index 4ab9a9ac5915..b07fc2bee3f9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java @@ -61,6 +61,8 @@ public final class CategoryKey { "com.android.settings.category.ia.my_device_info"; public static final String CATEGORY_BATTERY_SAVER_SETTINGS = "com.android.settings.category.ia.battery_saver_settings"; + public static final String CATEGORY_SMART_BATTERY_SETTINGS = + "com.android.settings.category.ia.smart_battery_settings"; public static final Map<String, String> KEY_COMPAT_MAP; diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java index 3c0f6fe8ccbb..ab7b54d98285 100644 --- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java +++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java @@ -136,7 +136,7 @@ public class DreamBackend { if (mDreamManager == null) return null; try { - return mDreamManager.getDefaultDreamComponent(); + return mDreamManager.getDefaultDreamComponentForUser(mContext.getUserId()); } catch (RemoteException e) { Log.w(TAG, "Failed to get default dream", e); return null; @@ -159,6 +159,25 @@ public class DreamBackend { return null; } + /** + * Gets an icon from active dream. + */ + public Drawable getActiveIcon() { + final ComponentName cn = getActiveDream(); + if (cn != null) { + final PackageManager pm = mContext.getPackageManager(); + try { + final ServiceInfo ri = pm.getServiceInfo(cn, 0); + if (ri != null) { + return ri.loadIcon(pm); + } + } catch (PackageManager.NameNotFoundException exc) { + return null; + } + } + return null; + } + public @WhenToDream int getWhenToDreamSetting() { if (!isEnabled()) { return NEVER; @@ -269,7 +288,7 @@ public class DreamBackend { if (mDreamManager == null || dreamInfo == null || dreamInfo.componentName == null) return; try { - mDreamManager.testDream(dreamInfo.componentName); + mDreamManager.testDream(mContext.getUserId(), dreamInfo.componentName); } catch (RemoteException e) { Log.w(TAG, "Failed to preview " + dreamInfo, e); } diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java index e19ac815b939..6d7e86f64944 100644 --- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.PowerManager; +import android.os.UserHandle; import android.provider.Settings.Global; import android.provider.Settings.Secure; import android.text.TextUtils; @@ -186,7 +187,8 @@ public class BatterySaverUtils { } private static void setBatterySaverConfirmationAcknowledged(Context context) { - Secure.putInt(context.getContentResolver(), Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 1); + Secure.putIntForUser(context.getContentResolver(), Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 1, + UserHandle.USER_CURRENT); } /** diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java new file mode 100644 index 000000000000..bc40903d88e4 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryStatus.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2019 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.fuelgauge; + +import static android.os.BatteryManager.BATTERY_HEALTH_UNKNOWN; +import static android.os.BatteryManager.BATTERY_STATUS_FULL; +import static android.os.BatteryManager.BATTERY_STATUS_UNKNOWN; +import static android.os.BatteryManager.EXTRA_HEALTH; +import static android.os.BatteryManager.EXTRA_LEVEL; +import static android.os.BatteryManager.EXTRA_MAX_CHARGING_CURRENT; +import static android.os.BatteryManager.EXTRA_MAX_CHARGING_VOLTAGE; +import static android.os.BatteryManager.EXTRA_PLUGGED; +import static android.os.BatteryManager.EXTRA_STATUS; + +import android.content.Context; +import android.content.Intent; +import android.os.BatteryManager; + +import com.android.settingslib.R; + +/** + * Stores and computes some battery information. + */ +public class BatteryStatus { + private static final int LOW_BATTERY_THRESHOLD = 20; + private static final int DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT = 5000000; + + public static final int CHARGING_UNKNOWN = -1; + public static final int CHARGING_SLOWLY = 0; + public static final int CHARGING_REGULAR = 1; + public static final int CHARGING_FAST = 2; + + public final int status; + public final int level; + public final int plugged; + public final int health; + public final int maxChargingWattage; + + public BatteryStatus(int status, int level, int plugged, int health, + int maxChargingWattage) { + this.status = status; + this.level = level; + this.plugged = plugged; + this.health = health; + this.maxChargingWattage = maxChargingWattage; + } + + public BatteryStatus(Intent batteryChangedIntent) { + status = batteryChangedIntent.getIntExtra(EXTRA_STATUS, BATTERY_STATUS_UNKNOWN); + plugged = batteryChangedIntent.getIntExtra(EXTRA_PLUGGED, 0); + level = batteryChangedIntent.getIntExtra(EXTRA_LEVEL, 0); + health = batteryChangedIntent.getIntExtra(EXTRA_HEALTH, BATTERY_HEALTH_UNKNOWN); + + final int maxChargingMicroAmp = batteryChangedIntent.getIntExtra(EXTRA_MAX_CHARGING_CURRENT, + -1); + int maxChargingMicroVolt = batteryChangedIntent.getIntExtra(EXTRA_MAX_CHARGING_VOLTAGE, -1); + + if (maxChargingMicroVolt <= 0) { + maxChargingMicroVolt = DEFAULT_CHARGING_VOLTAGE_MICRO_VOLT; + } + if (maxChargingMicroAmp > 0) { + // Calculating muW = muA * muV / (10^6 mu^2 / mu); splitting up the divisor + // to maintain precision equally on both factors. + maxChargingWattage = (maxChargingMicroAmp / 1000) + * (maxChargingMicroVolt / 1000); + } else { + maxChargingWattage = -1; + } + } + + /** + * Determine whether the device is plugged in (USB, power, or wireless). + * + * @return true if the device is plugged in. + */ + public boolean isPluggedIn() { + return plugged == BatteryManager.BATTERY_PLUGGED_AC + || plugged == BatteryManager.BATTERY_PLUGGED_USB + || plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS; + } + + /** + * Determine whether the device is plugged in (USB, power). + * + * @return true if the device is plugged in wired (as opposed to wireless) + */ + public boolean isPluggedInWired() { + return plugged == BatteryManager.BATTERY_PLUGGED_AC + || plugged == BatteryManager.BATTERY_PLUGGED_USB; + } + + /** + * Whether or not the device is charged. Note that some devices never return 100% for + * battery level, so this allows either battery level or status to determine if the + * battery is charged. + * + * @return true if the device is charged + */ + public boolean isCharged() { + return status == BATTERY_STATUS_FULL || level >= 100; + } + + /** + * Whether battery is low and needs to be charged. + * + * @return true if battery is low + */ + public boolean isBatteryLow() { + return level < LOW_BATTERY_THRESHOLD; + } + + /** + * Return current chargin speed is fast, slow or normal. + * + * @return the charing speed + */ + public final int getChargingSpeed(Context context) { + final int slowThreshold = context.getResources().getInteger( + R.integer.config_chargingSlowlyThreshold); + final int fastThreshold = context.getResources().getInteger( + R.integer.config_chargingFastThreshold); + return maxChargingWattage <= 0 ? CHARGING_UNKNOWN : + maxChargingWattage < slowThreshold ? CHARGING_SLOWLY : + maxChargingWattage > fastThreshold ? CHARGING_FAST : + CHARGING_REGULAR; + } + + @Override + public String toString() { + return "BatteryStatus{status=" + status + ",level=" + level + ",plugged=" + plugged + + ",health=" + health + ",maxChargingWattage=" + maxChargingWattage + "}"; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java index 55b6cda5548c..546095e9014a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java @@ -98,6 +98,7 @@ public class InputMethodPreference extends RestrictedSwitchPreference implements // Remove switch widget. setWidgetLayoutResource(NO_WIDGET); } + setIconSize(context.getResources().getDimensionPixelSize(R.dimen.secondary_app_icon_size)); } @VisibleForTesting diff --git a/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationApps.java b/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationApps.java index 104cc8f9841c..d3315efa0656 100644 --- a/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationApps.java +++ b/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationApps.java @@ -235,7 +235,7 @@ public class RecentLocationApps { public final CharSequence contentDescription; public final long requestFinishTime; - private Request(String packageName, UserHandle userHandle, Drawable icon, + public Request(String packageName, UserHandle userHandle, Drawable icon, CharSequence label, boolean isHighBattery, CharSequence contentDescription, long requestFinishTime) { this.packageName = packageName; diff --git a/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java b/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java index ff40d8e00603..450bdb161933 100644 --- a/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java +++ b/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java @@ -202,6 +202,12 @@ public class SettingsInjector { } /** + * Gives descendants a chance to log Preference click event + */ + protected void logPreferenceClick(Intent intent) { + } + + /** * Returns the settings parsed from the attributes of the * {@link SettingInjectorService#META_DATA_NAME} tag, or null. * @@ -315,6 +321,7 @@ public class SettingsInjector { // Settings > Location. Intent settingIntent = new Intent(); settingIntent.setClassName(mInfo.packageName, mInfo.settingsActivity); + logPreferenceClick(settingIntent); // Sometimes the user may navigate back to "Settings" and launch another different // injected setting after one injected setting has been launched. // diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java index 3a53d29f7618..00f94f5c2e64 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java @@ -19,8 +19,8 @@ import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.content.Context; import android.graphics.drawable.Drawable; -import android.util.Log; -import android.util.Pair; +import android.media.MediaRoute2Info; +import android.media.MediaRouter2Manager; import com.android.settingslib.R; import com.android.settingslib.bluetooth.BluetoothUtils; @@ -35,8 +35,9 @@ public class BluetoothMediaDevice extends MediaDevice { private CachedBluetoothDevice mCachedDevice; - BluetoothMediaDevice(Context context, CachedBluetoothDevice device) { - super(context, MediaDeviceType.TYPE_BLUETOOTH_DEVICE); + BluetoothMediaDevice(Context context, CachedBluetoothDevice device, + MediaRouter2Manager routerManager, MediaRoute2Info info, String packageName) { + super(context, routerManager, info, packageName); mCachedDevice = device; initDeviceRecord(); } @@ -55,28 +56,23 @@ public class BluetoothMediaDevice extends MediaDevice { @Override public Drawable getIcon() { - final Pair<Drawable, String> pair = BluetoothUtils - .getBtRainbowDrawableWithDescription(mContext, mCachedDevice); - return pair.first; - } - - @Override - public String getId() { - return MediaDeviceUtils.getId(mCachedDevice); + final Drawable drawable = getIconWithoutBackground(); + if (!isFastPairDevice()) { + setColorFilter(drawable); + } + return BluetoothUtils.buildAdvancedDrawable(mContext, drawable); } @Override - public boolean connect() { - //TODO(b/117129183): add callback to notify LocalMediaManager connection state. - final boolean isConnected = mCachedDevice.setActive(); - setConnectedRecord(); - Log.d(TAG, "connect() device : " + getName() + ", is selected : " + isConnected); - return isConnected; + public Drawable getIconWithoutBackground() { + return isFastPairDevice() + ? BluetoothUtils.getBtDrawableWithDescription(mContext, mCachedDevice).first + : mContext.getDrawable(R.drawable.ic_headphone); } @Override - public void disconnect() { - //TODO(b/117129183): disconnected last select device + public String getId() { + return MediaDeviceUtils.getId(mCachedDevice); } /** @@ -101,6 +97,13 @@ public class BluetoothMediaDevice extends MediaDevice { } @Override + public boolean isFastPairDevice() { + return mCachedDevice != null + && BluetoothUtils.getBooleanMetaData( + mCachedDevice.getDevice(), BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET); + } + + @Override public boolean isConnected() { return mCachedDevice.getBondState() == BluetoothDevice.BOND_BONDED && mCachedDevice.isConnected(); diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaManager.java deleted file mode 100644 index eb35c44bd690..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaManager.java +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright 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.media; - -import android.app.Notification; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothProfile; -import android.content.Context; -import android.util.Log; - -import com.android.settingslib.bluetooth.A2dpProfile; -import com.android.settingslib.bluetooth.BluetoothCallback; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; -import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; -import com.android.settingslib.bluetooth.HearingAidProfile; -import com.android.settingslib.bluetooth.LocalBluetoothManager; -import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; - -import java.util.ArrayList; -import java.util.List; - -/** - * BluetoothMediaManager provide interface to get Bluetooth device list. - */ -public class BluetoothMediaManager extends MediaManager implements BluetoothCallback, - LocalBluetoothProfileManager.ServiceListener { - - private static final String TAG = "BluetoothMediaManager"; - - private final DeviceAttributeChangeCallback mDeviceAttributeChangeCallback = - new DeviceAttributeChangeCallback(); - - private LocalBluetoothManager mLocalBluetoothManager; - private LocalBluetoothProfileManager mProfileManager; - private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager; - - private MediaDevice mLastAddedDevice; - private MediaDevice mLastRemovedDevice; - - private boolean mIsA2dpProfileReady = false; - private boolean mIsHearingAidProfileReady = false; - - BluetoothMediaManager(Context context, LocalBluetoothManager localBluetoothManager, - Notification notification) { - super(context, notification); - - mLocalBluetoothManager = localBluetoothManager; - mProfileManager = mLocalBluetoothManager.getProfileManager(); - mCachedBluetoothDeviceManager = mLocalBluetoothManager.getCachedDeviceManager(); - } - - @Override - public void startScan() { - mLocalBluetoothManager.getEventManager().registerCallback(this); - buildBluetoothDeviceList(); - dispatchDeviceListAdded(); - addServiceListenerIfNecessary(); - } - - private void addServiceListenerIfNecessary() { - // The profile may not ready when calling startScan(). - // Device status are all disconnected since profiles are not ready to connected. - // In this case, we observe onServiceConnected() in LocalBluetoothProfileManager. - // When A2dpProfile or HearingAidProfile is connected will call buildBluetoothDeviceList() - // again to find the connected devices. - if (!mIsA2dpProfileReady || !mIsHearingAidProfileReady) { - mProfileManager.addServiceListener(this); - } - } - - private void buildBluetoothDeviceList() { - mMediaDevices.clear(); - addConnectableA2dpDevices(); - addConnectableHearingAidDevices(); - } - - private void addConnectableA2dpDevices() { - final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); - if (a2dpProfile == null) { - Log.w(TAG, "addConnectableA2dpDevices() a2dp profile is null!"); - return; - } - - final List<BluetoothDevice> devices = a2dpProfile.getConnectableDevices(); - - for (BluetoothDevice device : devices) { - final CachedBluetoothDevice cachedDevice = - mCachedBluetoothDeviceManager.findDevice(device); - - if (cachedDevice == null) { - Log.w(TAG, "Can't found CachedBluetoothDevice : " + device.getName()); - continue; - } - - Log.d(TAG, "addConnectableA2dpDevices() device : " + cachedDevice.getName() - + ", is connected : " + cachedDevice.isConnected() - + ", is enabled : " + a2dpProfile.isEnabled(device)); - - if (a2dpProfile.isEnabled(device) - && BluetoothDevice.BOND_BONDED == cachedDevice.getBondState()) { - addMediaDevice(cachedDevice); - } - } - - mIsA2dpProfileReady = a2dpProfile.isProfileReady(); - } - - private void addConnectableHearingAidDevices() { - final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile(); - if (hapProfile == null) { - Log.w(TAG, "addConnectableHearingAidDevices() hap profile is null!"); - return; - } - - final List<Long> devicesHiSyncIds = new ArrayList<>(); - final List<BluetoothDevice> devices = hapProfile.getConnectableDevices(); - - for (BluetoothDevice device : devices) { - final CachedBluetoothDevice cachedDevice = - mCachedBluetoothDeviceManager.findDevice(device); - - if (cachedDevice == null) { - Log.w(TAG, "Can't found CachedBluetoothDevice : " + device.getName()); - continue; - } - - Log.d(TAG, "addConnectableHearingAidDevices() device : " + cachedDevice.getName() - + ", is connected : " + cachedDevice.isConnected() - + ", is enabled : " + hapProfile.isEnabled(device)); - - final long hiSyncId = hapProfile.getHiSyncId(device); - - // device with same hiSyncId should not be shown in the UI. - // So do not add it into connectedDevices. - if (!devicesHiSyncIds.contains(hiSyncId) && hapProfile.isEnabled(device) - && BluetoothDevice.BOND_BONDED == cachedDevice.getBondState()) { - devicesHiSyncIds.add(hiSyncId); - addMediaDevice(cachedDevice); - } - } - - mIsHearingAidProfileReady = hapProfile.isProfileReady(); - } - - private void addMediaDevice(CachedBluetoothDevice cachedDevice) { - MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(cachedDevice)); - if (mediaDevice == null) { - mediaDevice = new BluetoothMediaDevice(mContext, cachedDevice); - cachedDevice.registerCallback(mDeviceAttributeChangeCallback); - mLastAddedDevice = mediaDevice; - mMediaDevices.add(mediaDevice); - } - } - - @Override - public void stopScan() { - mLocalBluetoothManager.getEventManager().unregisterCallback(this); - unregisterDeviceAttributeChangeCallback(); - } - - private void unregisterDeviceAttributeChangeCallback() { - for (MediaDevice device : mMediaDevices) { - ((BluetoothMediaDevice) device).getCachedDevice() - .unregisterCallback(mDeviceAttributeChangeCallback); - } - } - - @Override - public void onBluetoothStateChanged(int bluetoothState) { - if (BluetoothAdapter.STATE_ON == bluetoothState) { - buildBluetoothDeviceList(); - dispatchDeviceListAdded(); - addServiceListenerIfNecessary(); - } else if (BluetoothAdapter.STATE_OFF == bluetoothState) { - final List<MediaDevice> removeDevicesList = new ArrayList<>(); - for (MediaDevice device : mMediaDevices) { - ((BluetoothMediaDevice) device).getCachedDevice() - .unregisterCallback(mDeviceAttributeChangeCallback); - removeDevicesList.add(device); - } - mMediaDevices.removeAll(removeDevicesList); - dispatchDeviceListRemoved(removeDevicesList); - } - } - - @Override - public void onAudioModeChanged() { - dispatchDataChanged(); - } - - @Override - public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { - if (isCachedDeviceConnected(cachedDevice)) { - addMediaDevice(cachedDevice); - dispatchDeviceAdded(cachedDevice); - } - } - - private boolean isCachedDeviceConnected(CachedBluetoothDevice cachedDevice) { - final boolean isConnectedHearingAidDevice = cachedDevice.isConnectedHearingAidDevice(); - final boolean isConnectedA2dpDevice = cachedDevice.isConnectedA2dpDevice(); - Log.d(TAG, "isCachedDeviceConnected() cachedDevice : " + cachedDevice - + ", is hearing aid connected : " + isConnectedHearingAidDevice - + ", is a2dp connected : " + isConnectedA2dpDevice); - - return isConnectedHearingAidDevice || isConnectedA2dpDevice; - } - - private void dispatchDeviceAdded(CachedBluetoothDevice cachedDevice) { - if (mLastAddedDevice != null - && MediaDeviceUtils.getId(cachedDevice) == mLastAddedDevice.getId()) { - dispatchDeviceAdded(mLastAddedDevice); - } - } - - @Override - public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { - if (!isCachedDeviceConnected(cachedDevice)) { - removeMediaDevice(cachedDevice); - dispatchDeviceRemoved(cachedDevice); - } - } - - private void removeMediaDevice(CachedBluetoothDevice cachedDevice) { - final MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(cachedDevice)); - if (mediaDevice != null) { - cachedDevice.unregisterCallback(mDeviceAttributeChangeCallback); - mLastRemovedDevice = mediaDevice; - mMediaDevices.remove(mediaDevice); - } - } - - void dispatchDeviceRemoved(CachedBluetoothDevice cachedDevice) { - if (mLastRemovedDevice != null - && MediaDeviceUtils.getId(cachedDevice) == mLastRemovedDevice.getId()) { - dispatchDeviceRemoved(mLastRemovedDevice); - } - } - - @Override - public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, - int bluetoothProfile) { - Log.d(TAG, "onProfileConnectionStateChanged() device: " + cachedDevice - + ", state: " + state + ", bluetoothProfile: " + bluetoothProfile); - - updateMediaDeviceListIfNecessary(cachedDevice); - } - - private void updateMediaDeviceListIfNecessary(CachedBluetoothDevice cachedDevice) { - if (BluetoothDevice.BOND_NONE == cachedDevice.getBondState()) { - removeMediaDevice(cachedDevice); - dispatchDeviceRemoved(cachedDevice); - } else { - if (findMediaDevice(MediaDeviceUtils.getId(cachedDevice)) != null) { - dispatchDataChanged(); - } - } - } - - @Override - public void onAclConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { - Log.d(TAG, "onAclConnectionStateChanged() device: " + cachedDevice + ", state: " + state); - - updateMediaDeviceListIfNecessary(cachedDevice); - } - - @Override - public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) { - Log.d(TAG, "onActiveDeviceChanged : device : " - + activeDevice + ", profile : " + bluetoothProfile); - - if (BluetoothProfile.HEARING_AID == bluetoothProfile) { - if (activeDevice != null) { - dispatchConnectedDeviceChanged(MediaDeviceUtils.getId(activeDevice)); - } - } else if (BluetoothProfile.A2DP == bluetoothProfile) { - // When active device change to Hearing Aid, - // BluetoothEventManager also send onActiveDeviceChanged() to notify that active device - // of A2DP profile is null. To handle this case, check hearing aid device - // is active device or not - final MediaDevice activeHearingAidDevice = findActiveHearingAidDevice(); - final String id = activeDevice == null - ? activeHearingAidDevice == null - ? PhoneMediaDevice.ID : activeHearingAidDevice.getId() - : MediaDeviceUtils.getId(activeDevice); - dispatchConnectedDeviceChanged(id); - } - } - - private MediaDevice findActiveHearingAidDevice() { - final HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); - - if (hearingAidProfile != null) { - final List<BluetoothDevice> activeDevices = hearingAidProfile.getActiveDevices(); - for (BluetoothDevice btDevice : activeDevices) { - if (btDevice != null) { - return findMediaDevice(MediaDeviceUtils.getId(btDevice)); - } - } - } - return null; - } - - @Override - public void onServiceConnected() { - if (!mIsA2dpProfileReady || !mIsHearingAidProfileReady) { - buildBluetoothDeviceList(); - dispatchDeviceListAdded(); - } - - //Remove the listener once a2dpProfile and hearingAidProfile are ready. - if (mIsA2dpProfileReady && mIsHearingAidProfileReady) { - mProfileManager.removeServiceListener(this); - } - } - - @Override - public void onServiceDisconnected() { - - } - - /** - * This callback is for update {@link BluetoothMediaDevice} summary when - * {@link CachedBluetoothDevice} connection state is changed. - */ - private class DeviceAttributeChangeCallback implements CachedBluetoothDevice.Callback { - - @Override - public void onDeviceAttributesChanged() { - dispatchDataChanged(); - } - } -} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java index 732e8dba3e44..949b2456042c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java @@ -15,15 +15,24 @@ */ package com.android.settingslib.media; +import static android.media.MediaRoute2Info.FEATURE_REMOTE_GROUP_PLAYBACK; +import static android.media.MediaRoute2Info.FEATURE_REMOTE_VIDEO_PLAYBACK; +import static android.media.MediaRoute2Info.TYPE_GROUP; +import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; + import android.content.Context; import android.graphics.drawable.Drawable; -import android.widget.Toast; +import android.media.MediaRoute2Info; +import android.media.MediaRouter2Manager; -import androidx.mediarouter.media.MediaRouter; +import androidx.annotation.VisibleForTesting; import com.android.settingslib.R; import com.android.settingslib.bluetooth.BluetoothUtils; +import java.util.List; + /** * InfoMediaDevice extends MediaDevice to represents wifi device. */ @@ -31,50 +40,73 @@ public class InfoMediaDevice extends MediaDevice { private static final String TAG = "InfoMediaDevice"; - private MediaRouter.RouteInfo mRouteInfo; - - InfoMediaDevice(Context context, MediaRouter.RouteInfo info) { - super(context, MediaDeviceType.TYPE_CAST_DEVICE); - mRouteInfo = info; + InfoMediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info, + String packageName) { + super(context, routerManager, info, packageName); initDeviceRecord(); } @Override public String getName() { - return mRouteInfo.getName(); + return mRouteInfo.getName().toString(); } @Override public String getSummary() { - return null; + return mRouteInfo.getClientPackageName() != null + ? mContext.getString(R.string.bluetooth_active_no_battery_level) : null; } @Override public Drawable getIcon() { - //TODO(b/120669861): Return remote device icon uri once api is ready. - return BluetoothUtils.buildBtRainbowDrawable(mContext, - mContext.getDrawable(R.drawable.ic_media_device), getId().hashCode()); + final Drawable drawable = getIconWithoutBackground(); + setColorFilter(drawable); + return BluetoothUtils.buildAdvancedDrawable(mContext, drawable); } @Override - public String getId() { - return MediaDeviceUtils.getId(mRouteInfo); + public Drawable getIconWithoutBackground() { + return mContext.getDrawable(getDrawableResIdByFeature()); } - @Override - public boolean connect() { - //TODO(b/121083246): use SystemApi to transfer media - setConnectedRecord(); - Toast.makeText(mContext, "This is cast device !", Toast.LENGTH_SHORT).show(); - return false; + @VisibleForTesting + int getDrawableResId() { + int resId; + switch (mRouteInfo.getType()) { + case TYPE_GROUP: + resId = R.drawable.ic_media_group_device; + break; + case TYPE_REMOTE_TV: + resId = R.drawable.ic_media_display_device; + break; + case TYPE_REMOTE_SPEAKER: + default: + resId = R.drawable.ic_media_speaker_device; + break; + } + return resId; } - @Override - public void disconnect() { - //TODO(b/121083246): disconnected last select device + @VisibleForTesting + int getDrawableResIdByFeature() { + int resId; + final List<String> features = mRouteInfo.getFeatures(); + if (features.contains(FEATURE_REMOTE_GROUP_PLAYBACK)) { + resId = R.drawable.ic_media_group_device; + } else if (features.contains(FEATURE_REMOTE_VIDEO_PLAYBACK)) { + resId = R.drawable.ic_media_display_device; + } else { + resId = R.drawable.ic_media_speaker_device; + } + + return resId; } @Override + public String getId() { + return MediaDeviceUtils.getId(mRouteInfo); + } + public boolean isConnected() { return true; } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java index bc8e2c35291d..6c7e03f104dd 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -15,14 +15,40 @@ */ package com.android.settingslib.media; +import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP; +import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_DOCK; +import static android.media.MediaRoute2Info.TYPE_GROUP; +import static android.media.MediaRoute2Info.TYPE_HDMI; +import static android.media.MediaRoute2Info.TYPE_HEARING_AID; +import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; +import static android.media.MediaRoute2Info.TYPE_UNKNOWN; +import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY; +import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; +import static android.media.MediaRoute2Info.TYPE_USB_HEADSET; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; +import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR; + import android.app.Notification; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; import android.content.Context; +import android.media.MediaRoute2Info; +import android.media.MediaRouter2Manager; +import android.media.RoutingSessionInfo; +import android.text.TextUtils; import android.util.Log; -import androidx.mediarouter.media.MediaRouteSelector; -import androidx.mediarouter.media.MediaRouter; - import com.android.internal.annotations.VisibleForTesting; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; /** * InfoMediaManager provide interface to get InfoMediaDevice list. @@ -30,65 +56,456 @@ import com.android.internal.annotations.VisibleForTesting; public class InfoMediaManager extends MediaManager { private static final String TAG = "InfoMediaManager"; - + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + @VisibleForTesting + final RouterManagerCallback mMediaRouterCallback = new RouterManagerCallback(); @VisibleForTesting - final MediaRouterCallback mMediaRouterCallback = new MediaRouterCallback(); + final Executor mExecutor = Executors.newSingleThreadExecutor(); @VisibleForTesting - MediaRouteSelector mSelector; + MediaRouter2Manager mRouterManager; @VisibleForTesting - MediaRouter mMediaRouter; + String mPackageName; - private String mPackageName; + private MediaDevice mCurrentConnectedDevice; + private LocalBluetoothManager mBluetoothManager; - InfoMediaManager(Context context, String packageName, Notification notification) { + public InfoMediaManager(Context context, String packageName, Notification notification, + LocalBluetoothManager localBluetoothManager) { super(context, notification); - mMediaRouter = MediaRouter.getInstance(context); - mPackageName = packageName; - mSelector = new MediaRouteSelector.Builder() - .addControlCategory(getControlCategoryByPackageName(mPackageName)) - .build(); + mRouterManager = MediaRouter2Manager.getInstance(context); + mBluetoothManager = localBluetoothManager; + if (!TextUtils.isEmpty(packageName)) { + mPackageName = packageName; + } } @Override public void startScan() { mMediaDevices.clear(); - mMediaRouter.addCallback(mSelector, mMediaRouterCallback, - MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); - } - - @VisibleForTesting - String getControlCategoryByPackageName(String packageName) { - //TODO(b/117129183): Use package name to get ControlCategory. - //Since api not ready, return fixed ControlCategory for prototype. - return "com.google.android.gms.cast.CATEGORY_CAST/4F8B3483"; + mRouterManager.registerCallback(mExecutor, mMediaRouterCallback); + refreshDevices(); } @Override public void stopScan() { - mMediaRouter.removeCallback(mMediaRouterCallback); + mRouterManager.unregisterCallback(mMediaRouterCallback); + } + + /** + * Get current device that played media. + * @return MediaDevice + */ + MediaDevice getCurrentConnectedDevice() { + return mCurrentConnectedDevice; + } + + /** + * Transfer MediaDevice for media without package name. + */ + boolean connectDeviceWithoutPackageName(MediaDevice device) { + boolean isConnected = false; + final List<RoutingSessionInfo> infos = mRouterManager.getActiveSessions(); + if (infos.size() > 0) { + final RoutingSessionInfo info = infos.get(0); + mRouterManager.transfer(info, device.mRouteInfo); + + isConnected = true; + } + return isConnected; + } + + /** + * Add a MediaDevice to let it play current media. + * + * @param device MediaDevice + * @return If add device successful return {@code true}, otherwise return {@code false} + */ + boolean addDeviceToPlayMedia(MediaDevice device) { + if (TextUtils.isEmpty(mPackageName)) { + Log.w(TAG, "addDeviceToPlayMedia() package name is null or empty!"); + return false; + } + + final RoutingSessionInfo info = getRoutingSessionInfo(); + if (info != null && info.getSelectableRoutes().contains(device.mRouteInfo.getId())) { + mRouterManager.selectRoute(info, device.mRouteInfo); + return true; + } + + Log.w(TAG, "addDeviceToPlayMedia() Ignoring selecting a non-selectable device : " + + device.getName()); + + return false; + } + + private RoutingSessionInfo getRoutingSessionInfo() { + final List<RoutingSessionInfo> sessionInfos = + mRouterManager.getRoutingSessions(mPackageName); + + return sessionInfos.get(sessionInfos.size() - 1); + } + + /** + * Remove a {@code device} from current media. + * + * @param device MediaDevice + * @return If device stop successful return {@code true}, otherwise return {@code false} + */ + boolean removeDeviceFromPlayMedia(MediaDevice device) { + if (TextUtils.isEmpty(mPackageName)) { + Log.w(TAG, "removeDeviceFromMedia() package name is null or empty!"); + return false; + } + + final RoutingSessionInfo info = getRoutingSessionInfo(); + if (info != null && info.getSelectedRoutes().contains(device.mRouteInfo.getId())) { + mRouterManager.deselectRoute(info, device.mRouteInfo); + return true; + } + + Log.w(TAG, "removeDeviceFromMedia() Ignoring deselecting a non-deselectable device : " + + device.getName()); + + return false; + } + + /** + * Release session to stop playing media on MediaDevice. + */ + boolean releaseSession() { + if (TextUtils.isEmpty(mPackageName)) { + Log.w(TAG, "releaseSession() package name is null or empty!"); + return false; + } + + final RoutingSessionInfo sessionInfo = getRoutingSessionInfo(); + + if (sessionInfo != null) { + mRouterManager.releaseSession(sessionInfo); + return true; + } + + Log.w(TAG, "releaseSession() Ignoring release session : " + mPackageName); + + return false; + } + + /** + * Get the MediaDevice list that can be added to current media. + * + * @return list of MediaDevice + */ + List<MediaDevice> getSelectableMediaDevice() { + final List<MediaDevice> deviceList = new ArrayList<>(); + if (TextUtils.isEmpty(mPackageName)) { + Log.w(TAG, "getSelectableMediaDevice() package name is null or empty!"); + return deviceList; + } + + final RoutingSessionInfo info = getRoutingSessionInfo(); + if (info != null) { + for (MediaRoute2Info route : mRouterManager.getSelectableRoutes(info)) { + deviceList.add(new InfoMediaDevice(mContext, mRouterManager, + route, mPackageName)); + } + return deviceList; + } + + Log.w(TAG, "getSelectableMediaDevice() cannot found selectable MediaDevice from : " + + mPackageName); + + return deviceList; + } + + /** + * Get the MediaDevice list that can be removed from current media session. + * + * @return list of MediaDevice + */ + List<MediaDevice> getDeselectableMediaDevice() { + final List<MediaDevice> deviceList = new ArrayList<>(); + if (TextUtils.isEmpty(mPackageName)) { + Log.d(TAG, "getDeselectableMediaDevice() package name is null or empty!"); + return deviceList; + } + + final RoutingSessionInfo info = getRoutingSessionInfo(); + if (info != null) { + for (MediaRoute2Info route : mRouterManager.getDeselectableRoutes(info)) { + deviceList.add(new InfoMediaDevice(mContext, mRouterManager, + route, mPackageName)); + Log.d(TAG, route.getName() + " is deselectable for " + mPackageName); + } + return deviceList; + } + Log.d(TAG, "getDeselectableMediaDevice() cannot found deselectable MediaDevice from : " + + mPackageName); + + return deviceList; + } + + /** + * Get the MediaDevice list that has been selected to current media. + * + * @return list of MediaDevice + */ + List<MediaDevice> getSelectedMediaDevice() { + final List<MediaDevice> deviceList = new ArrayList<>(); + if (TextUtils.isEmpty(mPackageName)) { + Log.w(TAG, "getSelectedMediaDevice() package name is null or empty!"); + return deviceList; + } + + final RoutingSessionInfo info = getRoutingSessionInfo(); + if (info != null) { + for (MediaRoute2Info route : mRouterManager.getSelectedRoutes(info)) { + deviceList.add(new InfoMediaDevice(mContext, mRouterManager, + route, mPackageName)); + } + return deviceList; + } + + Log.w(TAG, "getSelectedMediaDevice() cannot found selectable MediaDevice from : " + + mPackageName); + + return deviceList; + } + + void adjustSessionVolume(RoutingSessionInfo info, int volume) { + if (info == null) { + Log.w(TAG, "Unable to adjust session volume. RoutingSessionInfo is empty"); + return; + } + + mRouterManager.setSessionVolume(info, volume); } - class MediaRouterCallback extends MediaRouter.Callback { + /** + * Adjust the volume of {@link android.media.RoutingSessionInfo}. + * + * @param volume the value of volume + */ + void adjustSessionVolume(int volume) { + if (TextUtils.isEmpty(mPackageName)) { + Log.w(TAG, "adjustSessionVolume() package name is null or empty!"); + return; + } + + final RoutingSessionInfo info = getRoutingSessionInfo(); + if (info != null) { + Log.d(TAG, "adjustSessionVolume() adjust volume : " + volume + ", with : " + + mPackageName); + mRouterManager.setSessionVolume(info, volume); + return; + } + + Log.w(TAG, "adjustSessionVolume() can't found corresponding RoutingSession with : " + + mPackageName); + } + + /** + * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}. + * + * @return maximum volume of the session, and return -1 if not found. + */ + public int getSessionVolumeMax() { + if (TextUtils.isEmpty(mPackageName)) { + Log.w(TAG, "getSessionVolumeMax() package name is null or empty!"); + return -1; + } + + final RoutingSessionInfo info = getRoutingSessionInfo(); + if (info != null) { + return info.getVolumeMax(); + } + + Log.w(TAG, "getSessionVolumeMax() can't found corresponding RoutingSession with : " + + mPackageName); + return -1; + } + + /** + * Gets the current volume of the {@link android.media.RoutingSessionInfo}. + * + * @return current volume of the session, and return -1 if not found. + */ + public int getSessionVolume() { + if (TextUtils.isEmpty(mPackageName)) { + Log.w(TAG, "getSessionVolume() package name is null or empty!"); + return -1; + } + + final RoutingSessionInfo info = getRoutingSessionInfo(); + if (info != null) { + return info.getVolume(); + } + + Log.w(TAG, "getSessionVolume() can't found corresponding RoutingSession with : " + + mPackageName); + return -1; + } + + CharSequence getSessionName() { + if (TextUtils.isEmpty(mPackageName)) { + Log.w(TAG, "Unable to get session name. The package name is null or empty!"); + return null; + } + + final RoutingSessionInfo info = getRoutingSessionInfo(); + if (info != null) { + return info.getName(); + } + + Log.w(TAG, "Unable to get session name for package: " + mPackageName); + return null; + } + + private void refreshDevices() { + mMediaDevices.clear(); + mCurrentConnectedDevice = null; + if (TextUtils.isEmpty(mPackageName)) { + buildAllRoutes(); + } else { + buildAvailableRoutes(); + } + dispatchDeviceListAdded(); + } + + private void buildAllRoutes() { + for (MediaRoute2Info route : mRouterManager.getAllRoutes()) { + if (DEBUG) { + Log.d(TAG, "buildAllRoutes() route : " + route.getName() + ", volume : " + + route.getVolume() + ", type : " + route.getType()); + } + if (route.isSystemRoute()) { + addMediaDevice(route); + } + } + } + + List<RoutingSessionInfo> getActiveMediaSession() { + return mRouterManager.getActiveSessions(); + } + + private void buildAvailableRoutes() { + for (MediaRoute2Info route : mRouterManager.getAvailableRoutes(mPackageName)) { + if (DEBUG) { + Log.d(TAG, "buildAvailableRoutes() route : " + route.getName() + ", volume : " + + route.getVolume() + ", type : " + route.getType()); + } + addMediaDevice(route); + } + } + + @VisibleForTesting + void addMediaDevice(MediaRoute2Info route) { + final int deviceType = route.getType(); + MediaDevice mediaDevice = null; + switch (deviceType) { + case TYPE_UNKNOWN: + case TYPE_REMOTE_TV: + case TYPE_REMOTE_SPEAKER: + case TYPE_GROUP: + //TODO(b/148765806): use correct device type once api is ready. + mediaDevice = new InfoMediaDevice(mContext, mRouterManager, route, + mPackageName); + if (!TextUtils.isEmpty(mPackageName) + && getRoutingSessionInfo().getSelectedRoutes().contains(route.getId()) + && mCurrentConnectedDevice == null) { + mCurrentConnectedDevice = mediaDevice; + } + break; + case TYPE_BUILTIN_SPEAKER: + case TYPE_USB_DEVICE: + case TYPE_USB_HEADSET: + case TYPE_USB_ACCESSORY: + case TYPE_DOCK: + case TYPE_HDMI: + case TYPE_WIRED_HEADSET: + case TYPE_WIRED_HEADPHONES: + mediaDevice = + new PhoneMediaDevice(mContext, mRouterManager, route, mPackageName); + break; + case TYPE_HEARING_AID: + case TYPE_BLUETOOTH_A2DP: + final BluetoothDevice device = + BluetoothAdapter.getDefaultAdapter().getRemoteDevice(route.getAddress()); + final CachedBluetoothDevice cachedDevice = + mBluetoothManager.getCachedDeviceManager().findDevice(device); + if (cachedDevice != null) { + mediaDevice = new BluetoothMediaDevice(mContext, cachedDevice, mRouterManager, + route, mPackageName); + } + break; + default: + Log.w(TAG, "addMediaDevice() unknown device type : " + deviceType); + break; + + } + + if (mediaDevice != null) { + mMediaDevices.add(mediaDevice); + } + } + + class RouterManagerCallback extends MediaRouter2Manager.Callback { + @Override - public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) { - MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(route)); - if (mediaDevice == null) { - mediaDevice = new InfoMediaDevice(mContext, route); - Log.d(TAG, "onRouteAdded() route : " + route.getName()); - mMediaDevices.add(mediaDevice); - dispatchDeviceAdded(mediaDevice); + public void onRoutesAdded(List<MediaRoute2Info> routes) { + refreshDevices(); + } + + @Override + public void onPreferredFeaturesChanged(String packageName, List<String> preferredFeatures) { + if (TextUtils.equals(mPackageName, packageName)) { + refreshDevices(); } } @Override - public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo route) { - final MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(route)); - if (mediaDevice != null) { - Log.d(TAG, "onRouteRemoved() route : " + route.getName()); - mMediaDevices.remove(mediaDevice); - dispatchDeviceRemoved(mediaDevice); + public void onRoutesChanged(List<MediaRoute2Info> routes) { + refreshDevices(); + } + + @Override + public void onRoutesRemoved(List<MediaRoute2Info> routes) { + refreshDevices(); + } + + @Override + public void onTransferred(RoutingSessionInfo oldSession, RoutingSessionInfo newSession) { + if (DEBUG) { + Log.d(TAG, "onTransferred() oldSession : " + oldSession.getName() + + ", newSession : " + newSession.getName()); + } + mMediaDevices.clear(); + mCurrentConnectedDevice = null; + if (TextUtils.isEmpty(mPackageName)) { + buildAllRoutes(); + } else { + buildAvailableRoutes(); } + + final String id = mCurrentConnectedDevice != null + ? mCurrentConnectedDevice.getId() + : null; + dispatchConnectedDeviceChanged(id); + } + + @Override + public void onTransferFailed(RoutingSessionInfo session, MediaRoute2Info route) { + dispatchOnRequestFailed(REASON_UNKNOWN_ERROR); + } + + @Override + public void onRequestFailed(int reason) { + dispatchOnRequestFailed(reason); + } + + @Override + public void onSessionUpdated(RoutingSessionInfo sessionInfo) { + dispatchDataChanged(); } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java index 56b14c6b652d..9d06c8467e41 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java @@ -15,17 +15,27 @@ */ package com.android.settingslib.media; +import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR; + import android.app.Notification; -import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; import android.content.Context; +import android.media.RoutingSessionInfo; +import android.text.TextUtils; import android.util.Log; import androidx.annotation.IntDef; +import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.settingslib.bluetooth.A2dpProfile; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.HearingAidProfile; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfile; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -34,6 +44,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; /** * LocalMediaManager provide interface to get MediaDevice list and transfer media to MediaDevice. @@ -41,112 +52,143 @@ import java.util.List; public class LocalMediaManager implements BluetoothCallback { private static final Comparator<MediaDevice> COMPARATOR = Comparator.naturalOrder(); private static final String TAG = "LocalMediaManager"; + private static final int MAX_DISCONNECTED_DEVICE_NUM = 5; @Retention(RetentionPolicy.SOURCE) @IntDef({MediaDeviceState.STATE_CONNECTED, MediaDeviceState.STATE_CONNECTING, - MediaDeviceState.STATE_DISCONNECTED}) + MediaDeviceState.STATE_DISCONNECTED, + MediaDeviceState.STATE_CONNECTING_FAILED}) public @interface MediaDeviceState { - int STATE_CONNECTED = 1; - int STATE_CONNECTING = 2; - int STATE_DISCONNECTED = 3; + int STATE_CONNECTED = 0; + int STATE_CONNECTING = 1; + int STATE_DISCONNECTED = 2; + int STATE_CONNECTING_FAILED = 3; } - private final Collection<DeviceCallback> mCallbacks = new ArrayList<>(); + private final Collection<DeviceCallback> mCallbacks = new CopyOnWriteArrayList<>(); + private final Object mMediaDevicesLock = new Object(); @VisibleForTesting final MediaDeviceCallback mMediaDeviceCallback = new MediaDeviceCallback(); private Context mContext; - private BluetoothMediaManager mBluetoothMediaManager; private LocalBluetoothManager mLocalBluetoothManager; + private InfoMediaManager mInfoMediaManager; + private String mPackageName; + private MediaDevice mOnTransferBluetoothDevice; @VisibleForTesting - List<MediaDevice> mMediaDevices = new ArrayList<>(); + List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>(); + @VisibleForTesting + List<MediaDevice> mDisconnectedMediaDevices = new CopyOnWriteArrayList<>(); @VisibleForTesting MediaDevice mPhoneDevice; @VisibleForTesting MediaDevice mCurrentConnectedDevice; + @VisibleForTesting + DeviceAttributeChangeCallback mDeviceAttributeChangeCallback = + new DeviceAttributeChangeCallback(); + @VisibleForTesting + BluetoothAdapter mBluetoothAdapter; /** * Register to start receiving callbacks for MediaDevice events. */ public void registerCallback(DeviceCallback callback) { - synchronized (mCallbacks) { - mCallbacks.add(callback); - } + mCallbacks.add(callback); } /** * Unregister to stop receiving callbacks for MediaDevice events */ public void unregisterCallback(DeviceCallback callback) { - synchronized (mCallbacks) { - mCallbacks.remove(callback); - } + mCallbacks.remove(callback); } + /** + * Creates a LocalMediaManager with references to given managers. + * + * It will obtain a {@link LocalBluetoothManager} by calling + * {@link LocalBluetoothManager#getInstance} and create an {@link InfoMediaManager} passing + * that bluetooth manager. + * + * It will use {@link BluetoothAdapter#getDefaultAdapter()] for setting the bluetooth adapter. + */ public LocalMediaManager(Context context, String packageName, Notification notification) { mContext = context; + mPackageName = packageName; mLocalBluetoothManager = LocalBluetoothManager.getInstance(context, /* onInitCallback= */ null); + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (mLocalBluetoothManager == null) { Log.e(TAG, "Bluetooth is not supported on this device"); return; } - mBluetoothMediaManager = - new BluetoothMediaManager(context, mLocalBluetoothManager, notification); + mInfoMediaManager = + new InfoMediaManager(context, packageName, notification, mLocalBluetoothManager); } - @VisibleForTesting - LocalMediaManager(Context context, LocalBluetoothManager localBluetoothManager, - BluetoothMediaManager bluetoothMediaManager, InfoMediaManager infoMediaManager) { + /** + * Creates a LocalMediaManager with references to given managers. + * + * It will use {@link BluetoothAdapter#getDefaultAdapter()] for setting the bluetooth adapter. + */ + public LocalMediaManager(Context context, LocalBluetoothManager localBluetoothManager, + InfoMediaManager infoMediaManager, String packageName) { mContext = context; mLocalBluetoothManager = localBluetoothManager; - mBluetoothMediaManager = bluetoothMediaManager; + mInfoMediaManager = infoMediaManager; + mPackageName = packageName; + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); } /** * Connect the MediaDevice to transfer media * @param connectDevice the MediaDevice + * @return {@code true} if successfully call, otherwise return {@code false} */ - public void connectDevice(MediaDevice connectDevice) { - final MediaDevice device = getMediaDeviceById(mMediaDevices, connectDevice.getId()); + public boolean connectDevice(MediaDevice connectDevice) { + MediaDevice device = null; + synchronized (mMediaDevicesLock) { + device = getMediaDeviceById(mMediaDevices, connectDevice.getId()); + } + if (device == null) { + Log.w(TAG, "connectDevice() connectDevice not in the list!"); + return false; + } if (device instanceof BluetoothMediaDevice) { final CachedBluetoothDevice cachedDevice = ((BluetoothMediaDevice) device).getCachedDevice(); if (!cachedDevice.isConnected() && !cachedDevice.isBusy()) { + mOnTransferBluetoothDevice = connectDevice; + device.setState(MediaDeviceState.STATE_CONNECTING); cachedDevice.connect(); - return; + return true; } } if (device == mCurrentConnectedDevice) { Log.d(TAG, "connectDevice() this device all ready connected! : " + device.getName()); - return; + return false; } - //TODO(b/121083246): Update it once remote media API is ready. - if (mCurrentConnectedDevice != null && !(connectDevice instanceof InfoMediaDevice)) { + if (mCurrentConnectedDevice != null) { mCurrentConnectedDevice.disconnect(); } - final boolean isConnected = device.connect(); - if (isConnected) { - mCurrentConnectedDevice = device; + device.setState(MediaDeviceState.STATE_CONNECTING); + if (TextUtils.isEmpty(mPackageName)) { + mInfoMediaManager.connectDeviceWithoutPackageName(device); + } else { + device.connect(); } - - final int state = isConnected - ? MediaDeviceState.STATE_CONNECTED - : MediaDeviceState.STATE_DISCONNECTED; - dispatchSelectedDeviceStateChanged(device, state); + return true; } void dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state) { - synchronized (mCallbacks) { - for (DeviceCallback callback : mCallbacks) { - callback.onSelectedDeviceStateChanged(device, state); - } + for (DeviceCallback callback : getCallbacks()) { + callback.onSelectedDeviceStateChanged(device, state); } } @@ -154,34 +196,30 @@ public class LocalMediaManager implements BluetoothCallback { * Start scan connected MediaDevice */ public void startScan() { - mMediaDevices.clear(); - mBluetoothMediaManager.registerCallback(mMediaDeviceCallback); - mBluetoothMediaManager.startScan(); + synchronized (mMediaDevicesLock) { + mMediaDevices.clear(); + } + mInfoMediaManager.registerCallback(mMediaDeviceCallback); + mInfoMediaManager.startScan(); } - private void addPhoneDeviceIfNecessary() { - // add phone device to list if there have any Bluetooth device and cast device. - if (mMediaDevices.size() > 0 && !mMediaDevices.contains(mPhoneDevice)) { - if (mPhoneDevice == null) { - mPhoneDevice = new PhoneMediaDevice(mContext, mLocalBluetoothManager); - } - mMediaDevices.add(mPhoneDevice); + void dispatchDeviceListUpdate() { + final List<MediaDevice> mediaDevices = new ArrayList<>(mMediaDevices); + Collections.sort(mediaDevices, COMPARATOR); + for (DeviceCallback callback : getCallbacks()) { + callback.onDeviceListUpdate(mediaDevices); } } - private void removePhoneMediaDeviceIfNecessary() { - // if PhoneMediaDevice is the last item in the list, remove it. - if (mMediaDevices.size() == 1 && mMediaDevices.contains(mPhoneDevice)) { - mMediaDevices.clear(); + void dispatchDeviceAttributesChanged() { + for (DeviceCallback callback : getCallbacks()) { + callback.onDeviceAttributesChanged(); } } - void dispatchDeviceListUpdate() { - synchronized (mCallbacks) { - Collections.sort(mMediaDevices, COMPARATOR); - for (DeviceCallback callback : mCallbacks) { - callback.onDeviceListUpdate(new ArrayList<>(mMediaDevices)); - } + void dispatchOnRequestFailed(int reason) { + for (DeviceCallback callback : getCallbacks()) { + callback.onRequestFailed(reason); } } @@ -189,8 +227,9 @@ public class LocalMediaManager implements BluetoothCallback { * Stop scan MediaDevice */ public void stopScan() { - mBluetoothMediaManager.unregisterCallback(mMediaDeviceCallback); - mBluetoothMediaManager.stopScan(); + mInfoMediaManager.unregisterCallback(mMediaDeviceCallback); + mInfoMediaManager.stopScan(); + unRegisterDeviceAttributeChangeCallback(); } /** @@ -202,7 +241,7 @@ public class LocalMediaManager implements BluetoothCallback { */ public MediaDevice getMediaDeviceById(List<MediaDevice> devices, String id) { for (MediaDevice mediaDevice : devices) { - if (mediaDevice.getId().equals(id)) { + if (TextUtils.equals(mediaDevice.getId(), id)) { return mediaDevice; } } @@ -211,97 +250,353 @@ public class LocalMediaManager implements BluetoothCallback { } /** + * Find the MediaDevice from all media devices by id. + * + * @param id the unique id of MediaDevice + * @return MediaDevice + */ + public MediaDevice getMediaDeviceById(String id) { + synchronized (mMediaDevicesLock) { + for (MediaDevice mediaDevice : mMediaDevices) { + if (TextUtils.equals(mediaDevice.getId(), id)) { + return mediaDevice; + } + } + } + Log.i(TAG, "Unable to find device " + id); + return null; + } + + /** * Find the current connected MediaDevice. * * @return MediaDevice */ + @Nullable public MediaDevice getCurrentConnectedDevice() { return mCurrentConnectedDevice; } - private MediaDevice updateCurrentConnectedDevice() { - for (MediaDevice device : mMediaDevices) { - if (device instanceof BluetoothMediaDevice) { - if (isConnected(((BluetoothMediaDevice) device).getCachedDevice())) { - return device; + /** + * Add a MediaDevice to let it play current media. + * + * @param device MediaDevice + * @return If add device successful return {@code true}, otherwise return {@code false} + */ + public boolean addDeviceToPlayMedia(MediaDevice device) { + return mInfoMediaManager.addDeviceToPlayMedia(device); + } + + /** + * Remove a {@code device} from current media. + * + * @param device MediaDevice + * @return If device stop successful return {@code true}, otherwise return {@code false} + */ + public boolean removeDeviceFromPlayMedia(MediaDevice device) { + return mInfoMediaManager.removeDeviceFromPlayMedia(device); + } + + /** + * Get the MediaDevice list that can be added to current media. + * + * @return list of MediaDevice + */ + public List<MediaDevice> getSelectableMediaDevice() { + return mInfoMediaManager.getSelectableMediaDevice(); + } + + /** + * Get the MediaDevice list that can be removed from current media session. + * + * @return list of MediaDevice + */ + public List<MediaDevice> getDeselectableMediaDevice() { + return mInfoMediaManager.getDeselectableMediaDevice(); + } + + /** + * Release session to stop playing media on MediaDevice. + */ + public boolean releaseSession() { + return mInfoMediaManager.releaseSession(); + } + + /** + * Get the MediaDevice list that has been selected to current media. + * + * @return list of MediaDevice + */ + public List<MediaDevice> getSelectedMediaDevice() { + return mInfoMediaManager.getSelectedMediaDevice(); + } + + /** + * Adjust the volume of session. + * + * @param sessionId the value of media session id + * @param volume the value of volume + */ + public void adjustSessionVolume(String sessionId, int volume) { + final List<RoutingSessionInfo> infos = getActiveMediaSession(); + for (RoutingSessionInfo info : infos) { + if (TextUtils.equals(sessionId, info.getId())) { + mInfoMediaManager.adjustSessionVolume(info, volume); + return; + } + } + Log.w(TAG, "adjustSessionVolume: Unable to find session: " + sessionId); + } + + /** + * Adjust the volume of session. + * + * @param volume the value of volume + */ + public void adjustSessionVolume(int volume) { + mInfoMediaManager.adjustSessionVolume(volume); + } + + /** + * Gets the maximum volume of the {@link android.media.RoutingSessionInfo}. + * + * @return maximum volume of the session, and return -1 if not found. + */ + public int getSessionVolumeMax() { + return mInfoMediaManager.getSessionVolumeMax(); + } + + /** + * Gets the current volume of the {@link android.media.RoutingSessionInfo}. + * + * @return current volume of the session, and return -1 if not found. + */ + public int getSessionVolume() { + return mInfoMediaManager.getSessionVolume(); + } + + /** + * Gets the user-visible name of the {@link android.media.RoutingSessionInfo}. + * + * @return current name of the session, and return {@code null} if not found. + */ + public CharSequence getSessionName() { + return mInfoMediaManager.getSessionName(); + } + + /** + * Gets the current active session. + * + * @return current active session list{@link android.media.RoutingSessionInfo} + */ + public List<RoutingSessionInfo> getActiveMediaSession() { + return mInfoMediaManager.getActiveMediaSession(); + } + + /** + * Gets the current package name. + * + * @return current package name + */ + public String getPackageName() { + return mPackageName; + } + + @VisibleForTesting + MediaDevice updateCurrentConnectedDevice() { + MediaDevice connectedDevice = null; + synchronized (mMediaDevicesLock) { + for (MediaDevice device : mMediaDevices) { + if (device instanceof BluetoothMediaDevice) { + if (isActiveDevice(((BluetoothMediaDevice) device).getCachedDevice()) + && device.isConnected()) { + return device; + } + } else if (device instanceof PhoneMediaDevice) { + connectedDevice = device; } } } - return mMediaDevices.contains(mPhoneDevice) ? mPhoneDevice : null; + + return connectedDevice; + } + + private boolean isActiveDevice(CachedBluetoothDevice device) { + boolean isActiveDeviceA2dp = false; + boolean isActiveDeviceHearingAid = false; + final A2dpProfile a2dpProfile = mLocalBluetoothManager.getProfileManager().getA2dpProfile(); + if (a2dpProfile != null) { + isActiveDeviceA2dp = device.getDevice().equals(a2dpProfile.getActiveDevice()); + } + if (!isActiveDeviceA2dp) { + final HearingAidProfile hearingAidProfile = mLocalBluetoothManager.getProfileManager() + .getHearingAidProfile(); + if (hearingAidProfile != null) { + isActiveDeviceHearingAid = + hearingAidProfile.getActiveDevices().contains(device.getDevice()); + } + } + + return isActiveDeviceA2dp || isActiveDeviceHearingAid; } - private boolean isConnected(CachedBluetoothDevice device) { - return device.isActiveDevice(BluetoothProfile.A2DP) - || device.isActiveDevice(BluetoothProfile.HEARING_AID); + private Collection<DeviceCallback> getCallbacks() { + return new CopyOnWriteArrayList<>(mCallbacks); } class MediaDeviceCallback implements MediaManager.MediaDeviceCallback { @Override public void onDeviceAdded(MediaDevice device) { - if (!mMediaDevices.contains(device)) { - mMediaDevices.add(device); - addPhoneDeviceIfNecessary(); + boolean isAdded = false; + synchronized (mMediaDevicesLock) { + if (!mMediaDevices.contains(device)) { + mMediaDevices.add(device); + isAdded = true; + } + } + + if (isAdded) { dispatchDeviceListUpdate(); } } @Override public void onDeviceListAdded(List<MediaDevice> devices) { - for (MediaDevice device : devices) { - if (getMediaDeviceById(mMediaDevices, device.getId()) == null) { - mMediaDevices.add(device); - } + synchronized (mMediaDevicesLock) { + mMediaDevices.clear(); + mMediaDevices.addAll(devices); + mMediaDevices.addAll(buildDisconnectedBluetoothDevice()); } - addPhoneDeviceIfNecessary(); - mCurrentConnectedDevice = updateCurrentConnectedDevice(); - updatePhoneMediaDeviceSummary(); + + final MediaDevice infoMediaDevice = mInfoMediaManager.getCurrentConnectedDevice(); + mCurrentConnectedDevice = infoMediaDevice != null + ? infoMediaDevice : updateCurrentConnectedDevice(); dispatchDeviceListUpdate(); + if (mOnTransferBluetoothDevice != null && mOnTransferBluetoothDevice.isConnected()) { + connectDevice(mOnTransferBluetoothDevice); + mOnTransferBluetoothDevice.setState(MediaDeviceState.STATE_CONNECTED); + dispatchSelectedDeviceStateChanged(mOnTransferBluetoothDevice, + MediaDeviceState.STATE_CONNECTED); + mOnTransferBluetoothDevice = null; + } + } + + private List<MediaDevice> buildDisconnectedBluetoothDevice() { + if (mBluetoothAdapter == null) { + Log.w(TAG, "buildDisconnectedBluetoothDevice() BluetoothAdapter is null"); + return new ArrayList<>(); + } + + final List<BluetoothDevice> bluetoothDevices = + mBluetoothAdapter.getMostRecentlyConnectedDevices(); + final CachedBluetoothDeviceManager cachedDeviceManager = + mLocalBluetoothManager.getCachedDeviceManager(); + + final List<CachedBluetoothDevice> cachedBluetoothDeviceList = new ArrayList<>(); + int deviceCount = 0; + for (BluetoothDevice device : bluetoothDevices) { + final CachedBluetoothDevice cachedDevice = + cachedDeviceManager.findDevice(device); + if (cachedDevice != null) { + if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED + && !cachedDevice.isConnected() + && isA2dpOrHearingAidDevice(cachedDevice)) { + deviceCount++; + cachedBluetoothDeviceList.add(cachedDevice); + if (deviceCount >= MAX_DISCONNECTED_DEVICE_NUM) { + break; + } + } + } + } + + unRegisterDeviceAttributeChangeCallback(); + mDisconnectedMediaDevices.clear(); + for (CachedBluetoothDevice cachedDevice : cachedBluetoothDeviceList) { + final MediaDevice mediaDevice = new BluetoothMediaDevice(mContext, + cachedDevice, + null, null, mPackageName); + if (!mMediaDevices.contains(mediaDevice)) { + cachedDevice.registerCallback(mDeviceAttributeChangeCallback); + mDisconnectedMediaDevices.add(mediaDevice); + } + } + return new ArrayList<>(mDisconnectedMediaDevices); } - private void updatePhoneMediaDeviceSummary() { - if (mPhoneDevice != null) { - ((PhoneMediaDevice) mPhoneDevice) - .updateSummary(mCurrentConnectedDevice == mPhoneDevice); + private boolean isA2dpOrHearingAidDevice(CachedBluetoothDevice device) { + for (LocalBluetoothProfile profile : device.getConnectableProfiles()) { + if (profile instanceof A2dpProfile || profile instanceof HearingAidProfile) { + return true; + } } + return false; } @Override public void onDeviceRemoved(MediaDevice device) { - if (mMediaDevices.contains(device)) { - mMediaDevices.remove(device); - removePhoneMediaDeviceIfNecessary(); + boolean isRemoved = false; + synchronized (mMediaDevicesLock) { + if (mMediaDevices.contains(device)) { + mMediaDevices.remove(device); + isRemoved = true; + } + } + if (isRemoved) { dispatchDeviceListUpdate(); } } @Override public void onDeviceListRemoved(List<MediaDevice> devices) { - mMediaDevices.removeAll(devices); - removePhoneMediaDeviceIfNecessary(); + synchronized (mMediaDevicesLock) { + mMediaDevices.removeAll(devices); + } dispatchDeviceListUpdate(); } @Override public void onConnectedDeviceChanged(String id) { - final MediaDevice connectDevice = getMediaDeviceById(mMediaDevices, id); - - if (connectDevice == mCurrentConnectedDevice) { - Log.d(TAG, "onConnectedDeviceChanged() this device all ready connected!"); - return; + MediaDevice connectDevice = null; + synchronized (mMediaDevicesLock) { + connectDevice = getMediaDeviceById(mMediaDevices, id); } + connectDevice = connectDevice != null + ? connectDevice : updateCurrentConnectedDevice(); + mCurrentConnectedDevice = connectDevice; - updatePhoneMediaDeviceSummary(); - dispatchDeviceListUpdate(); + if (connectDevice != null) { + connectDevice.setState(MediaDeviceState.STATE_CONNECTED); + + dispatchSelectedDeviceStateChanged(mCurrentConnectedDevice, + MediaDeviceState.STATE_CONNECTED); + } } @Override public void onDeviceAttributesChanged() { - addPhoneDeviceIfNecessary(); - removePhoneMediaDeviceIfNecessary(); - dispatchDeviceListUpdate(); + dispatchDeviceAttributesChanged(); + } + + @Override + public void onRequestFailed(int reason) { + synchronized (mMediaDevicesLock) { + for (MediaDevice device : mMediaDevices) { + if (device.getState() == MediaDeviceState.STATE_CONNECTING) { + device.setState(MediaDeviceState.STATE_CONNECTING_FAILED); + } + } + } + dispatchOnRequestFailed(reason); } } + private void unRegisterDeviceAttributeChangeCallback() { + for (MediaDevice device : mDisconnectedMediaDevices) { + ((BluetoothMediaDevice) device).getCachedDevice() + .unregisterCallback(mDeviceAttributeChangeCallback); + } + } /** * Callback for notifying device information updating @@ -312,7 +607,7 @@ public class LocalMediaManager implements BluetoothCallback { * * @param devices MediaDevice list */ - void onDeviceListUpdate(List<MediaDevice> devices); + default void onDeviceListUpdate(List<MediaDevice> devices) {}; /** * Callback for notifying the connected device is changed. @@ -323,6 +618,46 @@ public class LocalMediaManager implements BluetoothCallback { * {@link MediaDeviceState#STATE_CONNECTING}, * {@link MediaDeviceState#STATE_DISCONNECTED} */ - void onSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state); + default void onSelectedDeviceStateChanged(MediaDevice device, + @MediaDeviceState int state) {}; + + /** + * Callback for notifying the device attributes is changed. + */ + default void onDeviceAttributesChanged() {}; + + /** + * Callback for notifying that transferring is failed. + * + * @param reason the reason that the request has failed. Can be one of followings: + * {@link android.media.MediaRoute2ProviderService#REASON_UNKNOWN_ERROR}, + * {@link android.media.MediaRoute2ProviderService#REASON_REJECTED}, + * {@link android.media.MediaRoute2ProviderService#REASON_NETWORK_ERROR}, + * {@link android.media.MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE}, + * {@link android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND}, + */ + default void onRequestFailed(int reason){}; + } + + /** + * This callback is for update {@link BluetoothMediaDevice} summary when + * {@link CachedBluetoothDevice} connection state is changed. + */ + @VisibleForTesting + class DeviceAttributeChangeCallback implements CachedBluetoothDevice.Callback { + + @Override + public void onDeviceAttributesChanged() { + if (mOnTransferBluetoothDevice != null + && !((BluetoothMediaDevice) mOnTransferBluetoothDevice).getCachedDevice() + .isBusy() + && !mOnTransferBluetoothDevice.isConnected()) { + // Failed to connect + mOnTransferBluetoothDevice.setState(MediaDeviceState.STATE_CONNECTING_FAILED); + mOnTransferBluetoothDevice = null; + dispatchOnRequestFailed(REASON_UNKNOWN_ERROR); + } + dispatchDeviceAttributesChanged(); + } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java index 53a852069478..126f9b91b0d2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java @@ -15,11 +15,34 @@ */ package com.android.settingslib.media; +import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP; +import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_DOCK; +import static android.media.MediaRoute2Info.TYPE_GROUP; +import static android.media.MediaRoute2Info.TYPE_HDMI; +import static android.media.MediaRoute2Info.TYPE_HEARING_AID; +import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; +import static android.media.MediaRoute2Info.TYPE_UNKNOWN; +import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY; +import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; +import static android.media.MediaRoute2Info.TYPE_USB_HEADSET; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; + import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; +import android.media.MediaRoute2Info; +import android.media.MediaRouter2Manager; import android.text.TextUtils; import androidx.annotation.IntDef; +import androidx.annotation.VisibleForTesting; + +import com.android.settingslib.R; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -31,23 +54,80 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { private static final String TAG = "MediaDevice"; @Retention(RetentionPolicy.SOURCE) - @IntDef({MediaDeviceType.TYPE_CAST_DEVICE, + @IntDef({MediaDeviceType.TYPE_UNKNOWN, + MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE, + MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE, + MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE, MediaDeviceType.TYPE_BLUETOOTH_DEVICE, + MediaDeviceType.TYPE_CAST_DEVICE, + MediaDeviceType.TYPE_CAST_GROUP_DEVICE, MediaDeviceType.TYPE_PHONE_DEVICE}) public @interface MediaDeviceType { - int TYPE_PHONE_DEVICE = 1; - int TYPE_CAST_DEVICE = 2; - int TYPE_BLUETOOTH_DEVICE = 3; + int TYPE_UNKNOWN = 0; + int TYPE_USB_C_AUDIO_DEVICE = 1; + int TYPE_3POINT5_MM_AUDIO_DEVICE = 2; + int TYPE_FAST_PAIR_BLUETOOTH_DEVICE = 3; + int TYPE_BLUETOOTH_DEVICE = 4; + int TYPE_CAST_DEVICE = 5; + int TYPE_CAST_GROUP_DEVICE = 6; + int TYPE_PHONE_DEVICE = 7; } + @VisibleForTesting + int mType; + private int mConnectedRecord; + private int mState; - protected Context mContext; - protected int mType; + protected final Context mContext; + protected final MediaRoute2Info mRouteInfo; + protected final MediaRouter2Manager mRouterManager; + protected final String mPackageName; - MediaDevice(Context context, @MediaDeviceType int type) { - mType = type; + MediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info, + String packageName) { mContext = context; + mRouteInfo = info; + mRouterManager = routerManager; + mPackageName = packageName; + setType(info); + } + + private void setType(MediaRoute2Info info) { + if (info == null) { + mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE; + return; + } + + switch (info.getType()) { + case TYPE_GROUP: + mType = MediaDeviceType.TYPE_CAST_GROUP_DEVICE; + break; + case TYPE_BUILTIN_SPEAKER: + mType = MediaDeviceType.TYPE_PHONE_DEVICE; + break; + case TYPE_WIRED_HEADSET: + case TYPE_WIRED_HEADPHONES: + mType = MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE; + break; + case TYPE_USB_DEVICE: + case TYPE_USB_HEADSET: + case TYPE_USB_ACCESSORY: + case TYPE_DOCK: + case TYPE_HDMI: + mType = MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE; + break; + case TYPE_HEARING_AID: + case TYPE_BLUETOOTH_A2DP: + mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE; + break; + case TYPE_UNKNOWN: + case TYPE_REMOTE_TV: + case TYPE_REMOTE_SPEAKER: + default: + mType = MediaDeviceType.TYPE_CAST_DEVICE; + break; + } } void initDeviceRecord() { @@ -56,6 +136,14 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { getId()); } + void setColorFilter(Drawable drawable) { + final ColorStateList list = + mContext.getResources().getColorStateList( + R.color.advanced_icon_color, mContext.getTheme()); + drawable.setColorFilter(new PorterDuffColorFilter(list.getDefaultColor(), + PorterDuff.Mode.SRC_IN)); + } + /** * Get name from MediaDevice. * @@ -78,48 +166,128 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { public abstract Drawable getIcon(); /** + * Get icon of MediaDevice without background. + * + * @return drawable of icon + */ + public abstract Drawable getIconWithoutBackground(); + + /** * Get unique ID that represent MediaDevice * @return unique id of MediaDevice */ public abstract String getId(); + void setConnectedRecord() { + mConnectedRecord++; + ConnectionRecordManager.getInstance().setConnectionRecord(mContext, getId(), + mConnectedRecord); + } + + /** + * According the MediaDevice type to check whether we are connected to this MediaDevice. + * + * @return Whether it is connected. + */ + public abstract boolean isConnected(); + + /** + * Request to set volume. + * + * @param volume is the new value. + */ + + public void requestSetVolume(int volume) { + mRouterManager.setRouteVolume(mRouteInfo, volume); + } + + /** + * Get max volume from MediaDevice. + * + * @return max volume. + */ + public int getMaxVolume() { + return mRouteInfo.getVolumeMax(); + } + + /** + * Get current volume from MediaDevice. + * + * @return current volume. + */ + public int getCurrentVolume() { + return mRouteInfo.getVolume(); + } + + /** + * Get application package name. + * + * @return package name. + */ + public String getClientPackageName() { + return mRouteInfo.getClientPackageName(); + } + + /** + * Get application label from MediaDevice. + * + * @return application label. + */ + public int getDeviceType() { + return mType; + } + /** * Transfer MediaDevice for media * * @return result of transfer media */ - public abstract boolean connect(); - - void setConnectedRecord() { - mConnectedRecord++; - ConnectionRecordManager.getInstance().setConnectionRecord(mContext, getId(), - mConnectedRecord); + public boolean connect() { + setConnectedRecord(); + mRouterManager.selectRoute(mPackageName, mRouteInfo); + return true; } /** * Stop transfer MediaDevice */ - public abstract void disconnect(); + public void disconnect() { + } /** - * According the MediaDevice type to check whether we are connected to this MediaDevice. + * Set current device's state + */ + public void setState(@LocalMediaManager.MediaDeviceState int state) { + mState = state; + } + + /** + * Get current device's state * - * @return Whether it is connected. + * @return state of device */ - public abstract boolean isConnected(); + public @LocalMediaManager.MediaDeviceState int getState() { + return mState; + } /** * Rules: - * 1. If there is one of the connected devices identified as a carkit, this carkit will - * be always on the top of the device list. Rule 2 and Rule 3 can’t overrule this rule. + * 1. If there is one of the connected devices identified as a carkit or fast pair device, + * the fast pair device will be always on the first of the device list and carkit will be + * second. Rule 2 and Rule 3 can’t overrule this rule. * 2. For devices without any usage data yet * WiFi device group sorted by alphabetical order + BT device group sorted by alphabetical * order + phone speaker * 3. For devices with usage record. * The most recent used one + device group with usage info sorted by how many times the * device has been used. - * 4. Phone device always in the top and the connected Bluetooth devices, cast devices and - * phone device will be always above on the disconnect Bluetooth devices. + * 4. The order is followed below rule: + * 1. USB-C audio device + * 2. 3.5 mm audio device + * 3. Bluetooth device + * 4. Cast device + * 5. Cast group device + * 6. Phone * * So the device list will look like 5 slots ranked as below. * Rule 4 + Rule 1 + the most recently used device + Rule 3 + Rule 2 @@ -139,39 +307,50 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { } } - // Phone device always in the top. - if (mType == MediaDeviceType.TYPE_PHONE_DEVICE) { - return -1; - } else if (another.mType == MediaDeviceType.TYPE_PHONE_DEVICE) { - return 1; - } - // Check carkit - if (isCarKitDevice()) { - return -1; - } else if (another.isCarKitDevice()) { - return 1; - } - // Set last used device at the first item - String lastSelectedDevice = ConnectionRecordManager.getInstance().getLastSelectedDevice(); - if (TextUtils.equals(lastSelectedDevice, getId())) { - return -1; - } else if (TextUtils.equals(lastSelectedDevice, another.getId())) { - return 1; - } - // Sort by how many times the device has been used if there is usage record - if ((mConnectedRecord != another.mConnectedRecord) - && (another.mConnectedRecord > 0 || mConnectedRecord > 0)) { - return (another.mConnectedRecord - mConnectedRecord); - } - // Both devices have never been used - // To devices with the same type, sort by alphabetical order if (mType == another.mType) { + // Check fast pair device + if (isFastPairDevice()) { + return -1; + } else if (another.isFastPairDevice()) { + return 1; + } + + // Check carkit + if (isCarKitDevice()) { + return -1; + } else if (another.isCarKitDevice()) { + return 1; + } + + // Set last used device at the first item + final String lastSelectedDevice = ConnectionRecordManager.getInstance() + .getLastSelectedDevice(); + if (TextUtils.equals(lastSelectedDevice, getId())) { + return -1; + } else if (TextUtils.equals(lastSelectedDevice, another.getId())) { + return 1; + } + // Sort by how many times the device has been used if there is usage record + if ((mConnectedRecord != another.mConnectedRecord) + && (another.mConnectedRecord > 0 || mConnectedRecord > 0)) { + return (another.mConnectedRecord - mConnectedRecord); + } + + // Both devices have never been used + // To devices with the same type, sort by alphabetical order final String s1 = getName(); final String s2 = another.getName(); return s1.compareToIgnoreCase(s2); + } else { + // Both devices have never been used, the priority is: + // 1. USB-C audio device + // 2. 3.5 mm audio device + // 3. Bluetooth device + // 4. Cast device + // 5. Cast group device + // 6. Phone + return mType < another.mType ? -1 : 1; } - // Both devices have never been used, the priority is Phone > Cast > Bluetooth - return mType - another.mType; } /** @@ -182,6 +361,14 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { return false; } + /** + * Check if it is FastPair device + * @return {@code true} if it is FastPair device, otherwise return {@code false} + */ + protected boolean isFastPairDevice() { + return false; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof MediaDevice)) { diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java index f181150de513..df6929e114ee 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java @@ -16,8 +16,7 @@ package com.android.settingslib.media; import android.bluetooth.BluetoothDevice; - -import androidx.mediarouter.media.MediaRouter; +import android.media.MediaRoute2Info; import com.android.settingslib.bluetooth.CachedBluetoothDevice; @@ -32,6 +31,9 @@ public class MediaDeviceUtils { * @return CachedBluetoothDevice address */ public static String getId(CachedBluetoothDevice cachedDevice) { + if (cachedDevice.isHearingAidDevice()) { + return Long.toString(cachedDevice.getHiSyncId()); + } return cachedDevice.getAddress(); } @@ -46,12 +48,12 @@ public class MediaDeviceUtils { } /** - * Use RouteInfo id to represent unique id + * Use MediaRoute2Info id to represent unique id * - * @param route the RouteInfo - * @return RouteInfo id + * @param route the MediaRoute2Info + * @return MediaRoute2Info id */ - public static String getId(MediaRouter.RouteInfo route) { + public static String getId(MediaRoute2Info route) { return route.getId(); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java index 7898982bccbc..e8cbab8197b2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java @@ -22,6 +22,7 @@ import android.util.Log; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; /** * MediaManager provide interface to get MediaDevice list. @@ -30,7 +31,7 @@ public abstract class MediaManager { private static final String TAG = "MediaManager"; - protected final Collection<MediaDeviceCallback> mCallbacks = new ArrayList<>(); + protected final Collection<MediaDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>(); protected final List<MediaDevice> mMediaDevices = new ArrayList<>(); protected Context mContext; @@ -42,18 +43,14 @@ public abstract class MediaManager { } protected void registerCallback(MediaDeviceCallback callback) { - synchronized (mCallbacks) { - if (!mCallbacks.contains(callback)) { - mCallbacks.add(callback); - } + if (!mCallbacks.contains(callback)) { + mCallbacks.add(callback); } } protected void unregisterCallback(MediaDeviceCallback callback) { - synchronized (mCallbacks) { - if (mCallbacks.contains(callback)) { - mCallbacks.remove(callback); - } + if (mCallbacks.contains(callback)) { + mCallbacks.remove(callback); } } @@ -78,53 +75,51 @@ public abstract class MediaManager { } protected void dispatchDeviceAdded(MediaDevice mediaDevice) { - synchronized (mCallbacks) { - for (MediaDeviceCallback callback : mCallbacks) { - callback.onDeviceAdded(mediaDevice); - } + for (MediaDeviceCallback callback : getCallbacks()) { + callback.onDeviceAdded(mediaDevice); } } protected void dispatchDeviceRemoved(MediaDevice mediaDevice) { - synchronized (mCallbacks) { - for (MediaDeviceCallback callback : mCallbacks) { - callback.onDeviceRemoved(mediaDevice); - } + for (MediaDeviceCallback callback : getCallbacks()) { + callback.onDeviceRemoved(mediaDevice); } } protected void dispatchDeviceListAdded() { - synchronized (mCallbacks) { - for (MediaDeviceCallback callback : mCallbacks) { - callback.onDeviceListAdded(new ArrayList<>(mMediaDevices)); - } + for (MediaDeviceCallback callback : getCallbacks()) { + callback.onDeviceListAdded(new ArrayList<>(mMediaDevices)); } } protected void dispatchDeviceListRemoved(List<MediaDevice> devices) { - synchronized (mCallbacks) { - for (MediaDeviceCallback callback : mCallbacks) { - callback.onDeviceListRemoved(devices); - } + for (MediaDeviceCallback callback : getCallbacks()) { + callback.onDeviceListRemoved(devices); } } protected void dispatchConnectedDeviceChanged(String id) { - synchronized (mCallbacks) { - for (MediaDeviceCallback callback : mCallbacks) { - callback.onConnectedDeviceChanged(id); - } + for (MediaDeviceCallback callback : getCallbacks()) { + callback.onConnectedDeviceChanged(id); } } protected void dispatchDataChanged() { - synchronized (mCallbacks) { - for (MediaDeviceCallback callback : mCallbacks) { - callback.onDeviceAttributesChanged(); - } + for (MediaDeviceCallback callback : getCallbacks()) { + callback.onDeviceAttributesChanged(); } } + protected void dispatchOnRequestFailed(int reason) { + for (MediaDeviceCallback callback : getCallbacks()) { + callback.onRequestFailed(reason); + } + } + + private Collection<MediaDeviceCallback> getCallbacks() { + return new CopyOnWriteArrayList<>(mCallbacks); + } + /** * Callback for notifying device is added, removed and attributes changed. */ @@ -169,5 +164,17 @@ public abstract class MediaManager { * (e.g: device name, connection state, subtitle) is changed. */ void onDeviceAttributesChanged(); + + /** + * Callback for notifying that transferring is failed. + * + * @param reason the reason that the request has failed. Can be one of followings: + * {@link android.media.MediaRoute2ProviderService#REASON_UNKNOWN_ERROR}, + * {@link android.media.MediaRoute2ProviderService#REASON_REJECTED}, + * {@link android.media.MediaRoute2ProviderService#REASON_NETWORK_ERROR}, + * {@link android.media.MediaRoute2ProviderService#REASON_ROUTE_NOT_AVAILABLE}, + * {@link android.media.MediaRoute2ProviderService#REASON_INVALID_COMMAND}, + */ + void onRequestFailed(int reason); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputSliceConstants.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputSliceConstants.java index e600cb892c44..2821af97ed98 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputSliceConstants.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputSliceConstants.java @@ -27,12 +27,39 @@ public class MediaOutputSliceConstants { public static final String KEY_MEDIA_OUTPUT = "media_output"; /** + * Key for the Media output group setting. + */ + public static final String KEY_MEDIA_OUTPUT_GROUP = "media_output_group"; + + /** + * Key for the Remote Media slice. + */ + public static final String KEY_REMOTE_MEDIA = "remote_media"; + + /** + * Key for the {@link android.media.session.MediaSession.Token}. + */ + public static final String KEY_MEDIA_SESSION_TOKEN = "key_media_session_token"; + + /** + * Key for the {@link android.media.RoutingSessionInfo#getId()} + */ + public static final String KEY_SESSION_INFO_ID = "key_session_info_id"; + + /** * Activity Action: Show a settings dialog containing {@link MediaDevice} to transfer media. */ public static final String ACTION_MEDIA_OUTPUT = "com.android.settings.panel.action.MEDIA_OUTPUT"; /** + * Activity Action: Show a settings dialog containing {@link MediaDevice} to handle media group + * operation. + */ + public static final String ACTION_MEDIA_OUTPUT_GROUP = + "com.android.settings.panel.action.MEDIA_OUTPUT_GROUP"; + + /** * An string extra specifying a media package name. */ public static final String EXTRA_PACKAGE_NAME = diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index af91c3464194..b6c0b30b3bd4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -15,16 +15,24 @@ */ package com.android.settingslib.media; +import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_DOCK; +import static android.media.MediaRoute2Info.TYPE_HDMI; +import static android.media.MediaRoute2Info.TYPE_USB_ACCESSORY; +import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; +import static android.media.MediaRoute2Info.TYPE_USB_HEADSET; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; + import android.content.Context; import android.graphics.drawable.Drawable; -import android.util.Log; +import android.media.MediaRoute2Info; +import android.media.MediaRouter2Manager; + +import androidx.annotation.VisibleForTesting; import com.android.settingslib.R; -import com.android.settingslib.bluetooth.A2dpProfile; import com.android.settingslib.bluetooth.BluetoothUtils; -import com.android.settingslib.bluetooth.HearingAidProfile; -import com.android.settingslib.bluetooth.LocalBluetoothManager; -import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; /** * PhoneMediaDevice extends MediaDevice to represents Phone device. @@ -33,23 +41,41 @@ public class PhoneMediaDevice extends MediaDevice { private static final String TAG = "PhoneMediaDevice"; - public static final String ID = "phone_media_device_id_1"; + public static final String PHONE_ID = "phone_media_device_id"; + // For 3.5 mm wired headset + public static final String WIRED_HEADSET_ID = "wired_headset_media_device_id"; + public static final String USB_HEADSET_ID = "usb_headset_media_device_id"; - private LocalBluetoothProfileManager mProfileManager; - private LocalBluetoothManager mLocalBluetoothManager; private String mSummary = ""; - PhoneMediaDevice(Context context, LocalBluetoothManager localBluetoothManager) { - super(context, MediaDeviceType.TYPE_PHONE_DEVICE); + PhoneMediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info, + String packageName) { + super(context, routerManager, info, packageName); - mLocalBluetoothManager = localBluetoothManager; - mProfileManager = mLocalBluetoothManager.getProfileManager(); initDeviceRecord(); } @Override public String getName() { - return mContext.getString(R.string.media_transfer_this_device_name); + CharSequence name; + switch (mRouteInfo.getType()) { + case TYPE_WIRED_HEADSET: + case TYPE_WIRED_HEADPHONES: + case TYPE_USB_DEVICE: + case TYPE_USB_HEADSET: + case TYPE_USB_ACCESSORY: + name = mContext.getString(R.string.media_transfer_wired_usb_device_name); + break; + case TYPE_DOCK: + case TYPE_HDMI: + name = mRouteInfo.getName(); + break; + case TYPE_BUILTIN_SPEAKER: + default: + name = mContext.getString(R.string.media_transfer_this_device_name); + break; + } + return name.toString(); } @Override @@ -59,39 +85,58 @@ public class PhoneMediaDevice extends MediaDevice { @Override public Drawable getIcon() { - return BluetoothUtils.buildBtRainbowDrawable(mContext, - mContext.getDrawable(R.drawable.ic_smartphone), getId().hashCode()); + final Drawable drawable = getIconWithoutBackground(); + setColorFilter(drawable); + return BluetoothUtils.buildAdvancedDrawable(mContext, drawable); } @Override - public String getId() { - return ID; + public Drawable getIconWithoutBackground() { + return mContext.getDrawable(getDrawableResId()); } - @Override - public boolean connect() { - final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile(); - final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); - - // Some device may not have HearingAidProfile, consider all situation to set active device. - boolean isConnected = false; - if (hapProfile != null && a2dpProfile != null) { - isConnected = hapProfile.setActiveDevice(null) && a2dpProfile.setActiveDevice(null); - } else if (a2dpProfile != null) { - isConnected = a2dpProfile.setActiveDevice(null); - } else if (hapProfile != null) { - isConnected = hapProfile.setActiveDevice(null); + @VisibleForTesting + int getDrawableResId() { + int resId; + switch (mRouteInfo.getType()) { + case TYPE_USB_DEVICE: + case TYPE_USB_HEADSET: + case TYPE_USB_ACCESSORY: + case TYPE_DOCK: + case TYPE_HDMI: + case TYPE_WIRED_HEADSET: + case TYPE_WIRED_HEADPHONES: + resId = R.drawable.ic_headphone; + break; + case TYPE_BUILTIN_SPEAKER: + default: + resId = R.drawable.ic_smartphone; + break; } - updateSummary(isConnected); - setConnectedRecord(); - - Log.d(TAG, "connect() device : " + getName() + ", is selected : " + isConnected); - return isConnected; + return resId; } @Override - public void disconnect() { - updateSummary(false); + public String getId() { + String id; + switch (mRouteInfo.getType()) { + case TYPE_WIRED_HEADSET: + case TYPE_WIRED_HEADPHONES: + id = WIRED_HEADSET_ID; + break; + case TYPE_USB_DEVICE: + case TYPE_USB_HEADSET: + case TYPE_USB_ACCESSORY: + case TYPE_DOCK: + case TYPE_HDMI: + id = USB_HEADSET_ID; + break; + case TYPE_BUILTIN_SPEAKER: + default: + id = PHONE_ID; + break; + } + return id; } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/net/DataUsageUtils.java b/packages/SettingsLib/src/com/android/settingslib/net/DataUsageUtils.java index 853c77efd4e9..b1234f291b74 100644 --- a/packages/SettingsLib/src/com/android/settingslib/net/DataUsageUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/net/DataUsageUtils.java @@ -18,11 +18,15 @@ package com.android.settingslib.net; import android.content.Context; import android.net.NetworkTemplate; +import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.util.Log; import com.android.internal.util.ArrayUtils; + +import java.util.List; + /** * Utils class for data usage */ @@ -33,26 +37,42 @@ public class DataUsageUtils { * Return mobile NetworkTemplate based on {@code subId} */ public static NetworkTemplate getMobileTemplate(Context context, int subId) { - final TelephonyManager telephonyManager = context.getSystemService( - TelephonyManager.class); - final SubscriptionManager subscriptionManager = context.getSystemService( - SubscriptionManager.class); - final NetworkTemplate mobileAll = NetworkTemplate.buildTemplateMobileAll( - telephonyManager.createForSubscriptionId(subId).getSubscriberId()); - - if (!subscriptionManager.isActiveSubId(subId)) { - Log.i(TAG, "Subscription is not active: " + subId); - return mobileAll; + final TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class); + final int mobileDefaultSubId = telephonyManager.getSubscriptionId(); + + final SubscriptionManager subscriptionManager = + context.getSystemService(SubscriptionManager.class); + final List<SubscriptionInfo> subInfoList = + subscriptionManager.getAvailableSubscriptionInfoList(); + if (subInfoList == null) { + Log.i(TAG, "Subscription is not inited: " + subId); + return getMobileTemplateForSubId(telephonyManager, mobileDefaultSubId); } - final String[] mergedSubscriberIds = telephonyManager.createForSubscriptionId(subId) - .getMergedImsisFromGroup(); + for (SubscriptionInfo subInfo : subInfoList) { + if ((subInfo != null) && (subInfo.getSubscriptionId() == subId)) { + return getNormalizedMobileTemplate(telephonyManager, subId); + } + } + Log.i(TAG, "Subscription is not active: " + subId); + return getMobileTemplateForSubId(telephonyManager, mobileDefaultSubId); + } + private static NetworkTemplate getNormalizedMobileTemplate( + TelephonyManager telephonyManager, int subId) { + final NetworkTemplate mobileTemplate = getMobileTemplateForSubId(telephonyManager, subId); + final String[] mergedSubscriberIds = telephonyManager + .createForSubscriptionId(subId).getMergedImsisFromGroup(); if (ArrayUtils.isEmpty(mergedSubscriberIds)) { Log.i(TAG, "mergedSubscriberIds is null."); - return mobileAll; + return mobileTemplate; } - return NetworkTemplate.normalize(mobileAll, mergedSubscriberIds); + return NetworkTemplate.normalize(mobileTemplate, mergedSubscriberIds); + } + + private static NetworkTemplate getMobileTemplateForSubId( + TelephonyManager telephonyManager, int subId) { + return NetworkTemplate.buildTemplateMobileAll(telephonyManager.getSubscriberId(subId)); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/net/UidDetailProvider.java b/packages/SettingsLib/src/com/android/settingslib/net/UidDetailProvider.java index e3516158daac..dad82ee61e08 100644 --- a/packages/SettingsLib/src/com/android/settingslib/net/UidDetailProvider.java +++ b/packages/SettingsLib/src/com/android/settingslib/net/UidDetailProvider.java @@ -63,7 +63,7 @@ public class UidDetailProvider { } public UidDetailProvider(Context context) { - mContext = context.getApplicationContext(); + mContext = context; mUidDetailCache = new SparseArray<UidDetail>(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java new file mode 100644 index 000000000000..549bc8a455cf --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/notification/ConversationIconFactory.java @@ -0,0 +1,236 @@ +/* + * 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.notification; + +import android.annotation.ColorInt; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.LauncherApps; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.util.IconDrawableFactory; +import android.util.Log; + +import com.android.launcher3.icons.BaseIconFactory; +import com.android.settingslib.R; + +/** + * Factory for creating normalized conversation icons. + * We are not using Launcher's IconFactory because conversation rendering only runs on the UI + * thread, so there is no need to manage a pool across multiple threads. Launcher's rendering + * also includes shadows, which are only appropriate on top of wallpaper, not embedded in UI. + */ +public class ConversationIconFactory extends BaseIconFactory { + // Geometry of the various parts of the design. All values are 1dp on a 56x56dp icon grid. + // Space is left around the "head" (main avatar) for + // ........ + // .HHHHHH. + // .HHHrrrr + // .HHHrBBr + // ....rrrr + // This is trying to recreate the view layout in notification_template_material_conversation.xml + + private static final float HEAD_SIZE = 52f; + private static final float BADGE_SIZE = 12f; + private static final float BADGE_CENTER = 46f; + private static final float CIRCLE_MARGIN = 36f; + private static final float BADGE_ORIGIN = HEAD_SIZE - BADGE_SIZE; // 40f + private static final float BASE_ICON_SIZE = 56f; + + private static final float OUT_CIRCLE_DIA = (BASE_ICON_SIZE - CIRCLE_MARGIN); // 20f + private static final float INN_CIRCLE_DIA = (float) Math.sqrt(2 * BADGE_SIZE * BADGE_SIZE) ; + private static final float OUT_CIRCLE_RAD = OUT_CIRCLE_DIA / 2; + private static final float INN_CIRCLE_RAD = INN_CIRCLE_DIA / 2; + // Android draws strokes centered on the radius, so our actual radius is an avg of the outside + // and inside of the ring stroke + private static final float CIRCLE_RADIUS = + INN_CIRCLE_RAD + ((OUT_CIRCLE_RAD - INN_CIRCLE_RAD) / 2); + private static final float RING_STROKE_WIDTH = (OUT_CIRCLE_DIA - INN_CIRCLE_DIA) / 2; + + final LauncherApps mLauncherApps; + final PackageManager mPackageManager; + final IconDrawableFactory mIconDrawableFactory; + private int mImportantConversationColor; + + public ConversationIconFactory(Context context, LauncherApps la, PackageManager pm, + IconDrawableFactory iconDrawableFactory, int iconSizePx) { + super(context, context.getResources().getConfiguration().densityDpi, + iconSizePx); + mLauncherApps = la; + mPackageManager = pm; + mIconDrawableFactory = iconDrawableFactory; + mImportantConversationColor = context.getResources().getColor( + R.color.important_conversation, null); + } + + /** + * Returns the conversation info drawable + */ + public Drawable getBaseIconDrawable(ShortcutInfo shortcutInfo) { + return mLauncherApps.getShortcutIconDrawable(shortcutInfo, mFillResIconDpi); + } + + /** + * Get the {@link Drawable} that represents the app icon, badged with the work profile icon + * if appropriate. + */ + public Drawable getAppBadge(String packageName, int userId) { + Drawable badge = null; + try { + final ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser( + packageName, PackageManager.GET_META_DATA, userId); + badge = mIconDrawableFactory.getBadgedIcon(appInfo, userId); + } catch (PackageManager.NameNotFoundException e) { + badge = mPackageManager.getDefaultActivityIcon(); + } + return badge; + } + + /** + * Returns a {@link Drawable} for the entire conversation. The shortcut icon will be badged + * with the launcher icon of the app specified by packageName. + */ + public Drawable getConversationDrawable(ShortcutInfo info, String packageName, int uid, + boolean important) { + return getConversationDrawable(getBaseIconDrawable(info), packageName, uid, important); + } + + /** + * Returns a {@link Drawable} for the entire conversation. The drawable will be badged + * with the launcher icon of the app specified by packageName. + */ + public Drawable getConversationDrawable(Drawable baseIcon, String packageName, int uid, + boolean important) { + return new ConversationIconDrawable(baseIcon, + getAppBadge(packageName, UserHandle.getUserId(uid)), + mIconBitmapSize, + mImportantConversationColor, + important); + } + + /** + * Custom Drawable that overlays a badge drawable (e.g. notification small icon or app icon) on + * a base icon (conversation/person avatar), plus decorations indicating conversation + * importance. + */ + public static class ConversationIconDrawable extends Drawable { + private Drawable mBaseIcon; + private Drawable mBadgeIcon; + private int mIconSize; + private Paint mRingPaint; + private boolean mShowRing; + private Paint mPaddingPaint; + + public ConversationIconDrawable(Drawable baseIcon, + Drawable badgeIcon, + int iconSize, + @ColorInt int ringColor, + boolean showImportanceRing) { + mBaseIcon = baseIcon; + mBadgeIcon = badgeIcon; + mIconSize = iconSize; + mShowRing = showImportanceRing; + mRingPaint = new Paint(); + mRingPaint.setStyle(Paint.Style.STROKE); + mRingPaint.setColor(ringColor); + mPaddingPaint = new Paint(); + mPaddingPaint.setStyle(Paint.Style.FILL_AND_STROKE); + mPaddingPaint.setColor(Color.WHITE); + } + + /** + * Show or hide the importance ring. + */ + public void setImportant(boolean important) { + if (important != mShowRing) { + mShowRing = important; + invalidateSelf(); + } + } + + @Override + public int getIntrinsicWidth() { + return mIconSize; + } + + @Override + public int getIntrinsicHeight() { + return mIconSize; + } + + // Similar to badgeWithDrawable, but relying on the bounds of each underlying drawable + @Override + public void draw(Canvas canvas) { + final Rect bounds = getBounds(); + + // scale to our internal grid + final float scale = bounds.width() / BASE_ICON_SIZE; + final int ringStrokeWidth = (int) (RING_STROKE_WIDTH * scale); + final int headSize = (int) (HEAD_SIZE * scale); + final int badgePadding = (int) (BADGE_ORIGIN * scale); + final int badgeCenter = (int) (BADGE_CENTER * scale); + + mPaddingPaint.setStrokeWidth(ringStrokeWidth); + final float radius = (int) (CIRCLE_RADIUS * scale); // stroke outside + if (mBaseIcon != null) { + mBaseIcon.setBounds(0, + 0, + headSize , + headSize); + mBaseIcon.draw(canvas); + } else { + Log.w("ConversationIconFactory", "ConversationIconDrawable has null base icon"); + } + if (mBadgeIcon != null) { + canvas.drawCircle(badgeCenter, badgeCenter, radius, mPaddingPaint); + mBadgeIcon.setBounds( + badgePadding, + badgePadding, + headSize, + headSize); + mBadgeIcon.draw(canvas); + } else { + Log.w("ConversationIconFactory", "ConversationIconDrawable has null badge icon"); + } + if (mShowRing) { + mRingPaint.setStrokeWidth(ringStrokeWidth); + canvas.drawCircle(badgeCenter, badgeCenter, radius, mRingPaint); + } + } + + @Override + public void setAlpha(int alpha) { + // unimplemented + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + // unimplemented + } + + @Override + public int getOpacity() { + return 0; + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java index f14def1afce5..a210e90a3cfc 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java @@ -33,6 +33,7 @@ import android.util.Log; import android.util.Slog; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.LinearLayout; @@ -328,7 +329,6 @@ public class EnableZenModeDialog { boolean enabled, int rowId, Uri conditionId) { if (tag.lines == null) { tag.lines = row.findViewById(android.R.id.content); - tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); } if (tag.line1 == null) { tag.line1 = (TextView) row.findViewById(android.R.id.text1); @@ -358,44 +358,46 @@ public class EnableZenModeDialog { } }); - // minus button - final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1); - button1.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onClickTimeButton(row, tag, false /*down*/, rowId); - } - }); - - // plus button - final ImageView button2 = (ImageView) row.findViewById(android.R.id.button2); - button2.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onClickTimeButton(row, tag, true /*up*/, rowId); - } - }); - final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId); + final ImageView minusButton = (ImageView) row.findViewById(android.R.id.button1); + final ImageView plusButton = (ImageView) row.findViewById(android.R.id.button2); if (rowId == COUNTDOWN_CONDITION_INDEX && time > 0) { - button1.setVisibility(View.VISIBLE); - button2.setVisibility(View.VISIBLE); + minusButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onClickTimeButton(row, tag, false /*down*/, rowId); + tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + } + }); + + plusButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onClickTimeButton(row, tag, true /*up*/, rowId); + tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + } + }); if (mBucketIndex > -1) { - button1.setEnabled(mBucketIndex > 0); - button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1); + minusButton.setEnabled(mBucketIndex > 0); + plusButton.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1); } else { final long span = time - System.currentTimeMillis(); - button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS); + minusButton.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS); final Condition maxCondition = ZenModeConfig.toTimeCondition(mContext, MAX_BUCKET_MINUTES, ActivityManager.getCurrentUser()); - button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary)); + plusButton.setEnabled(!Objects.equals(condition.summary, maxCondition.summary)); } - button1.setAlpha(button1.isEnabled() ? 1f : .5f); - button2.setAlpha(button2.isEnabled() ? 1f : .5f); + minusButton.setAlpha(minusButton.isEnabled() ? 1f : .5f); + plusButton.setAlpha(plusButton.isEnabled() ? 1f : .5f); } else { - button1.setVisibility(View.GONE); - button2.setVisibility(View.GONE); + if (minusButton != null) { + ((ViewGroup) row).removeView(minusButton); + } + + if (plusButton != null) { + ((ViewGroup) row).removeView(plusButton); + } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/ZenDurationDialog.java b/packages/SettingsLib/src/com/android/settingslib/notification/ZenDurationDialog.java index 66ee8021957f..87e97b17b914 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/ZenDurationDialog.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/ZenDurationDialog.java @@ -25,6 +25,7 @@ import android.service.notification.Condition; import android.service.notification.ZenModeConfig; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.LinearLayout; @@ -228,37 +229,40 @@ public class ZenDurationDialog { } private void updateButtons(ConditionTag tag, View row, int rowIndex) { - // minus button - final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1); - button1.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onClickTimeButton(row, tag, false /*down*/, rowIndex); - } - }); - - // plus button - final ImageView button2 = (ImageView) row.findViewById(android.R.id.button2); - button2.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onClickTimeButton(row, tag, true /*up*/, rowIndex); - } - }); - + final ImageView minusButton = (ImageView) row.findViewById(android.R.id.button1); + final ImageView plusButton = (ImageView) row.findViewById(android.R.id.button2); final long time = tag.countdownZenDuration; if (rowIndex == COUNTDOWN_CONDITION_INDEX) { - button1.setVisibility(View.VISIBLE); - button2.setVisibility(View.VISIBLE); + minusButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onClickTimeButton(row, tag, false /*down*/, rowIndex); + tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + } + }); - button1.setEnabled(time > MIN_BUCKET_MINUTES); - button2.setEnabled(tag.countdownZenDuration != MAX_BUCKET_MINUTES); + plusButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onClickTimeButton(row, tag, true /*up*/, rowIndex); + tag.lines.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + } + }); + minusButton.setVisibility(View.VISIBLE); + plusButton.setVisibility(View.VISIBLE); - button1.setAlpha(button1.isEnabled() ? 1f : .5f); - button2.setAlpha(button2.isEnabled() ? 1f : .5f); + minusButton.setEnabled(time > MIN_BUCKET_MINUTES); + plusButton.setEnabled(tag.countdownZenDuration != MAX_BUCKET_MINUTES); + + minusButton.setAlpha(minusButton.isEnabled() ? 1f : .5f); + plusButton.setAlpha(plusButton.isEnabled() ? 1f : .5f); } else { - button1.setVisibility(View.GONE); - button2.setVisibility(View.GONE); + if (minusButton != null) { + ((ViewGroup) row).removeView(minusButton); + } + if (plusButton != null) { + ((ViewGroup) row).removeView(plusButton); + } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/ColorUtil.java b/packages/SettingsLib/src/com/android/settingslib/utils/ColorUtil.java new file mode 100644 index 000000000000..c54b471135d1 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/utils/ColorUtil.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2019 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.utils; + +import android.content.Context; +import android.content.res.TypedArray; + +/** Utility class for getting color attribute **/ +public class ColorUtil { + + /** + * Returns android:disabledAlpha value in context + */ + public static float getDisabledAlpha(Context context) { + final TypedArray ta = context.obtainStyledAttributes( + new int[]{android.R.attr.disabledAlpha}); + final float alpha = ta.getFloat(0, 0); + ta.recycle(); + return alpha; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java index 7046d234904b..0a70f72518d4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java +++ b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java @@ -119,7 +119,7 @@ public class PowerUtil { return null; } if (drainTimeMs <= ONE_DAY_MILLIS) { - return context.getString(R.string.power_suggestion_extend_battery, + return context.getString(R.string.power_suggestion_battery_run_out, getDateTimeStringFromMs(context, drainTimeMs)); } else { return getMoreThanOneDayShortString(context, drainTimeMs, diff --git a/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreference.java b/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreference.java index a31b71e2cd0b..15576182c53a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreference.java @@ -16,11 +16,14 @@ package com.android.settingslib.widget; +import android.annotation.StringRes; import android.content.Context; +import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.util.AttributeSet; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.core.content.res.TypedArrayUtils; import androidx.preference.Preference; import androidx.preference.PreferenceViewHolder; @@ -55,9 +58,84 @@ public class FooterPreference extends Preference { title.setLongClickable(false); } + @Override + public void setSummary(CharSequence summary) { + setTitle(summary); + } + + @Override + public void setSummary(int summaryResId) { + setTitle(summaryResId); + } + + @Override + public CharSequence getSummary() { + return getTitle(); + } + private void init() { - setIcon(R.drawable.ic_info_outline_24); - setKey(KEY_FOOTER); + if (getIcon() == null) { + setIcon(R.drawable.ic_info_outline_24); + } setOrder(ORDER_FOOTER); + if (TextUtils.isEmpty(getKey())) { + setKey(KEY_FOOTER); + } + } + + /** + * The builder is convenient to creat a dynamic FooterPreference. + */ + public static class Builder { + private Context mContext; + private String mKey; + private CharSequence mTitle; + + public Builder(@NonNull Context context) { + mContext = context; + } + + /** + * To set the key value of the {@link FooterPreference}. + * @param key The key value. + */ + public Builder setKey(@NonNull String key) { + mKey = key; + return this; + } + + /** + * To set the title of the {@link FooterPreference}. + * @param title The title. + */ + public Builder setTitle(CharSequence title) { + mTitle = title; + return this; + } + + /** + * To set the title of the {@link FooterPreference}. + * @param titleResId The resource id of the title. + */ + public Builder setTitle(@StringRes int titleResId) { + mTitle = mContext.getText(titleResId); + return this; + } + + /** + * To generate the {@link FooterPreference}. + */ + public FooterPreference build() { + final FooterPreference footerPreference = new FooterPreference(mContext); + footerPreference.setSelectable(false); + if (TextUtils.isEmpty(mTitle)) { + throw new IllegalArgumentException("Footer title cannot be empty!"); + } + footerPreference.setTitle(mTitle); + if (!TextUtils.isEmpty(mKey)) { + footerPreference.setKey(mKey); + } + return footerPreference; + } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixin.java b/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixin.java deleted file mode 100644 index fcf236337703..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixin.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.widget; - -import android.content.Context; - -import androidx.preference.PreferenceFragment; -import androidx.preference.PreferenceScreen; - -import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.SetPreferenceScreen; - -/** - * Framework mixin is deprecated, use the compat version instead. - * - * @deprecated - */ -@Deprecated -public class FooterPreferenceMixin implements LifecycleObserver, SetPreferenceScreen { - - private final PreferenceFragment mFragment; - private FooterPreference mFooterPreference; - - public FooterPreferenceMixin(PreferenceFragment fragment, Lifecycle lifecycle) { - mFragment = fragment; - lifecycle.addObserver(this); - } - - @Override - public void setPreferenceScreen(PreferenceScreen preferenceScreen) { - if (mFooterPreference != null) { - preferenceScreen.addPreference(mFooterPreference); - } - } - - /** - * Creates a new {@link FooterPreference}. - */ - public FooterPreference createFooterPreference() { - final PreferenceScreen screen = mFragment.getPreferenceScreen(); - if (mFooterPreference != null && screen != null) { - screen.removePreference(mFooterPreference); - } - mFooterPreference = new FooterPreference(getPrefContext()); - - if (screen != null) { - screen.addPreference(mFooterPreference); - } - return mFooterPreference; - } - - /** - * Returns an UI context with theme properly set for new Preference objects. - */ - private Context getPrefContext() { - return mFragment.getPreferenceManager().getContext(); - } - - public boolean hasFooter() { - return mFooterPreference != null; - } -} - diff --git a/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixinCompat.java b/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixinCompat.java deleted file mode 100644 index d45e56d486ae..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixinCompat.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.widget; - -import android.content.Context; - -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceScreen; - -import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.SetPreferenceScreen; - -public class FooterPreferenceMixinCompat implements LifecycleObserver, SetPreferenceScreen { - - private final PreferenceFragmentCompat mFragment; - private FooterPreference mFooterPreference; - - public FooterPreferenceMixinCompat(PreferenceFragmentCompat fragment, Lifecycle lifecycle) { - mFragment = fragment; - lifecycle.addObserver(this); - } - - @Override - public void setPreferenceScreen(PreferenceScreen preferenceScreen) { - if (mFooterPreference != null) { - preferenceScreen.addPreference(mFooterPreference); - } - } - - /** - * Creates a new {@link FooterPreference}. - */ - public FooterPreference createFooterPreference() { - final PreferenceScreen screen = mFragment.getPreferenceScreen(); - if (mFooterPreference != null && screen != null) { - screen.removePreference(mFooterPreference); - } - mFooterPreference = new FooterPreference(getPrefContext()); - - if (screen != null) { - screen.addPreference(mFooterPreference); - } - return mFooterPreference; - } - - /** - * Returns an UI context with theme properly set for new Preference objects. - */ - private Context getPrefContext() { - return mFragment.getPreferenceManager().getContext(); - } - - public boolean hasFooter() { - return mFooterPreference != null; - } -} - diff --git a/packages/SettingsLib/src/com/android/settingslib/widget/UpdatableListPreferenceDialogFragment.java b/packages/SettingsLib/src/com/android/settingslib/widget/UpdatableListPreferenceDialogFragment.java new file mode 100644 index 000000000000..bd86d673c0d1 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/widget/UpdatableListPreferenceDialogFragment.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2019 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.widget; + +import android.content.res.TypedArray; +import android.os.Bundle; +import android.widget.ArrayAdapter; + +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.preference.ListPreference; +import androidx.preference.PreferenceDialogFragmentCompat; + +import com.android.settingslib.core.instrumentation.Instrumentable; + +import java.util.ArrayList; + +/** + * {@link PreferenceDialogFragmentCompat} that updates the available options + * when {@code onListPreferenceUpdated} is called." + */ +public class UpdatableListPreferenceDialogFragment extends PreferenceDialogFragmentCompat implements + Instrumentable { + + private static final String SAVE_STATE_INDEX = "UpdatableListPreferenceDialogFragment.index"; + private static final String SAVE_STATE_ENTRIES = + "UpdatableListPreferenceDialogFragment.entries"; + private static final String SAVE_STATE_ENTRY_VALUES = + "UpdatableListPreferenceDialogFragment.entryValues"; + private static final String METRICS_CATEGORY_KEY = "metrics_category_key"; + private ArrayAdapter mAdapter; + private int mClickedDialogEntryIndex; + private ArrayList<CharSequence> mEntries; + private CharSequence[] mEntryValues; + private int mMetricsCategory = METRICS_CATEGORY_UNKNOWN; + + /** + * Creates a new instance of {@link UpdatableListPreferenceDialogFragment}. + */ + public static UpdatableListPreferenceDialogFragment newInstance( + String key, int metricsCategory) { + UpdatableListPreferenceDialogFragment fragment = + new UpdatableListPreferenceDialogFragment(); + Bundle args = new Bundle(1); + args.putString(ARG_KEY, key); + args.putInt(METRICS_CATEGORY_KEY, metricsCategory); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Bundle bundle = getArguments(); + mMetricsCategory = + bundle.getInt(METRICS_CATEGORY_KEY, METRICS_CATEGORY_UNKNOWN); + if (savedInstanceState == null) { + mEntries = new ArrayList<>(); + setPreferenceData(getListPreference()); + } else { + mClickedDialogEntryIndex = savedInstanceState.getInt(SAVE_STATE_INDEX, 0); + mEntries = savedInstanceState.getCharSequenceArrayList(SAVE_STATE_ENTRIES); + mEntryValues = + savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRY_VALUES); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(SAVE_STATE_INDEX, mClickedDialogEntryIndex); + outState.putCharSequenceArrayList(SAVE_STATE_ENTRIES, mEntries); + outState.putCharSequenceArray(SAVE_STATE_ENTRY_VALUES, mEntryValues); + } + + @Override + public void onDialogClosed(boolean positiveResult) { + if (positiveResult && mClickedDialogEntryIndex >= 0) { + final ListPreference preference = getListPreference(); + final String value = mEntryValues[mClickedDialogEntryIndex].toString(); + if (preference.callChangeListener(value)) { + preference.setValue(value); + } + } + } + + @VisibleForTesting + void setAdapter(ArrayAdapter adapter) { + mAdapter = adapter; + } + + @VisibleForTesting + void setEntries(ArrayList<CharSequence> entries) { + mEntries = entries; + } + + @VisibleForTesting + ArrayAdapter getAdapter() { + return mAdapter; + } + + @VisibleForTesting + void setMetricsCategory(Bundle bundle) { + mMetricsCategory = + bundle.getInt(METRICS_CATEGORY_KEY, METRICS_CATEGORY_UNKNOWN); + } + + @Override + protected void onPrepareDialogBuilder(Builder builder) { + super.onPrepareDialogBuilder(builder); + final TypedArray a = getContext().obtainStyledAttributes( + null, + com.android.internal.R.styleable.AlertDialog, + com.android.internal.R.attr.alertDialogStyle, 0); + + mAdapter = new ArrayAdapter<>( + getContext(), + a.getResourceId( + com.android.internal.R.styleable.AlertDialog_singleChoiceItemLayout, + com.android.internal.R.layout.select_dialog_singlechoice), + mEntries); + + builder.setSingleChoiceItems(mAdapter, mClickedDialogEntryIndex, + (dialog, which) -> { + mClickedDialogEntryIndex = which; + onClick(dialog, -1); + dialog.dismiss(); + }); + builder.setPositiveButton(null, null); + a.recycle(); + } + + @Override + public int getMetricsCategory() { + return mMetricsCategory; + } + + @VisibleForTesting + ListPreference getListPreference() { + return (ListPreference) getPreference(); + } + + private void setPreferenceData(ListPreference preference) { + mEntries.clear(); + mClickedDialogEntryIndex = preference.findIndexOfValue(preference.getValue()); + for (CharSequence entry : preference.getEntries()) { + mEntries.add(entry); + } + mEntryValues = preference.getEntryValues(); + } + + /** + * Update new data set for list preference. + */ + public void onListPreferenceUpdated(ListPreference preference) { + if (mAdapter != null) { + setPreferenceData(preference); + mAdapter.notifyDataSetChanged(); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java index 09c8f7398199..8968340b65f4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java @@ -16,6 +16,9 @@ package com.android.settingslib.wifi; +import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLED; +import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_PERMANENTLY_DISABLED; + import android.annotation.IntDef; import android.annotation.MainThread; import android.annotation.Nullable; @@ -33,11 +36,9 @@ import android.net.NetworkKey; import android.net.NetworkScoreManager; import android.net.NetworkScorerAppData; import android.net.ScoredNetwork; -import android.net.wifi.IWifiManager; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiConfiguration.KeyMgmt; -import android.net.wifi.WifiEnterpriseConfig; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiNetworkScoreCache; @@ -47,7 +48,6 @@ import android.net.wifi.hotspot2.ProvisioningCallback; import android.os.Bundle; import android.os.Parcelable; import android.os.RemoteException; -import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; @@ -83,7 +83,16 @@ import java.util.concurrent.atomic.AtomicInteger; * <p>An AccessPoint, which would be more fittingly named "WifiNetwork", is an aggregation of * {@link ScanResult ScanResults} along with pertinent metadata (e.g. current connection info, * network scores) required to successfully render the network to the user. + * + * @deprecated WifiTracker/AccessPoint is no longer supported, and will be removed in a future + * release. Clients that need a dynamic list of available wifi networks should migrate to one of the + * newer tracker classes, + * {@link com.android.wifitrackerlib.WifiPickerTracker}, + * {@link com.android.wifitrackerlib.SavedNetworkTracker}, + * {@link com.android.wifitrackerlib.NetworkDetailsTracker}, + * in conjunction with {@link com.android.wifitrackerlib.WifiEntry} to represent each wifi network. */ +@Deprecated public class AccessPoint implements Comparable<AccessPoint> { static final String TAG = "SettingsLib.AccessPoint"; @@ -143,6 +152,16 @@ public class AccessPoint implements Comparable<AccessPoint> { int VERY_FAST = 30; } + @IntDef({PasspointConfigurationVersion.INVALID, + PasspointConfigurationVersion.NO_OSU_PROVISIONED, + PasspointConfigurationVersion.OSU_PROVISIONED}) + @Retention(RetentionPolicy.SOURCE) + public @interface PasspointConfigurationVersion { + int INVALID = 0; + int NO_OSU_PROVISIONED = 1; // R1. + int OSU_PROVISIONED = 2; // R2 or R3. + } + /** The underlying set of scan results comprising this AccessPoint. */ @GuardedBy("mLock") private final ArraySet<ScanResult> mScanResults = new ArraySet<>(); @@ -171,12 +190,13 @@ public class AccessPoint implements Comparable<AccessPoint> { static final String KEY_SCANRESULTS = "key_scanresults"; static final String KEY_SCOREDNETWORKCACHE = "key_scorednetworkcache"; static final String KEY_CONFIG = "key_config"; + static final String KEY_PASSPOINT_UNIQUE_ID = "key_passpoint_unique_id"; static final String KEY_FQDN = "key_fqdn"; static final String KEY_PROVIDER_FRIENDLY_NAME = "key_provider_friendly_name"; - static final String KEY_IS_CARRIER_AP = "key_is_carrier_ap"; - static final String KEY_CARRIER_AP_EAP_TYPE = "key_carrier_ap_eap_type"; - static final String KEY_CARRIER_NAME = "key_carrier_name"; static final String KEY_EAPTYPE = "eap_psktype"; + static final String KEY_SUBSCRIPTION_EXPIRATION_TIME_IN_MILLIS = + "key_subscription_expiration_time_in_millis"; + static final String KEY_PASSPOINT_CONFIGURATION_VERSION = "key_passpoint_configuration_version"; static final String KEY_IS_PSK_SAE_TRANSITION_MODE = "key_is_psk_sae_transition_mode"; static final String KEY_IS_OWE_TRANSITION_MODE = "key_is_owe_transition_mode"; static final AtomicInteger sLastId = new AtomicInteger(0); @@ -204,17 +224,10 @@ public class AccessPoint implements Comparable<AccessPoint> { private static final int EAP_WPA = 1; // WPA-EAP private static final int EAP_WPA2_WPA3 = 2; // RSN-EAP - /** - * The number of distinct wifi levels. - * - * <p>Must keep in sync with {@link R.array.wifi_signal} and {@link WifiManager#RSSI_LEVELS}. - */ - public static final int SIGNAL_LEVELS = 5; - public static final int UNREACHABLE_RSSI = Integer.MIN_VALUE; public static final String KEY_PREFIX_AP = "AP:"; - public static final String KEY_PREFIX_FQDN = "FQDN:"; + public static final String KEY_PREFIX_PASSPOINT_UNIQUE_ID = "PASSPOINT:"; public static final String KEY_PREFIX_OSU = "OSU:"; private final Context mContext; @@ -247,11 +260,13 @@ public class AccessPoint implements Comparable<AccessPoint> { * Information associated with the {@link PasspointConfiguration}. Only maintaining * the relevant info to preserve spaces. */ + private String mPasspointUniqueId; private String mFqdn; private String mProviderFriendlyName; private boolean mIsRoaming = false; - - private boolean mIsCarrierAp = false; + private long mSubscriptionExpirationTimeInMillis; + @PasspointConfigurationVersion private int mPasspointConfigurationVersion = + PasspointConfigurationVersion.INVALID; private OsuProvider mOsuProvider; @@ -262,12 +277,6 @@ public class AccessPoint implements Comparable<AccessPoint> { private boolean mIsPskSaeTransitionMode = false; private boolean mIsOweTransitionMode = false; - /** - * The EAP type {@link WifiEnterpriseConfig.Eap} associated with this AP if it is a carrier AP. - */ - private int mCarrierApEapType = WifiEnterpriseConfig.Eap.NONE; - private String mCarrierName = null; - public AccessPoint(Context context, Bundle savedState) { mContext = context; @@ -310,20 +319,21 @@ public class AccessPoint implements Comparable<AccessPoint> { mScoredNetworkCache.put(timedScore.getScore().networkKey.wifiKey.bssid, timedScore); } } + if (savedState.containsKey(KEY_PASSPOINT_UNIQUE_ID)) { + mPasspointUniqueId = savedState.getString(KEY_PASSPOINT_UNIQUE_ID); + } if (savedState.containsKey(KEY_FQDN)) { mFqdn = savedState.getString(KEY_FQDN); } if (savedState.containsKey(KEY_PROVIDER_FRIENDLY_NAME)) { mProviderFriendlyName = savedState.getString(KEY_PROVIDER_FRIENDLY_NAME); } - if (savedState.containsKey(KEY_IS_CARRIER_AP)) { - mIsCarrierAp = savedState.getBoolean(KEY_IS_CARRIER_AP); - } - if (savedState.containsKey(KEY_CARRIER_AP_EAP_TYPE)) { - mCarrierApEapType = savedState.getInt(KEY_CARRIER_AP_EAP_TYPE); + if (savedState.containsKey(KEY_SUBSCRIPTION_EXPIRATION_TIME_IN_MILLIS)) { + mSubscriptionExpirationTimeInMillis = + savedState.getLong(KEY_SUBSCRIPTION_EXPIRATION_TIME_IN_MILLIS); } - if (savedState.containsKey(KEY_CARRIER_NAME)) { - mCarrierName = savedState.getString(KEY_CARRIER_NAME); + if (savedState.containsKey(KEY_PASSPOINT_CONFIGURATION_VERSION)) { + mPasspointConfigurationVersion = savedState.getInt(KEY_PASSPOINT_CONFIGURATION_VERSION); } if (savedState.containsKey(KEY_IS_PSK_SAE_TRANSITION_MODE)) { mIsPskSaeTransitionMode = savedState.getBoolean(KEY_IS_PSK_SAE_TRANSITION_MODE); @@ -355,8 +365,15 @@ public class AccessPoint implements Comparable<AccessPoint> { */ public AccessPoint(Context context, PasspointConfiguration config) { mContext = context; + mPasspointUniqueId = config.getUniqueId(); mFqdn = config.getHomeSp().getFqdn(); mProviderFriendlyName = config.getHomeSp().getFriendlyName(); + mSubscriptionExpirationTimeInMillis = config.getSubscriptionExpirationTimeMillis(); + if (config.isOsuProvisioned()) { + mPasspointConfigurationVersion = PasspointConfigurationVersion.OSU_PROVISIONED; + } else { + mPasspointConfigurationVersion = PasspointConfigurationVersion.NO_OSU_PROVISIONED; + } updateKey(); } @@ -369,6 +386,7 @@ public class AccessPoint implements Comparable<AccessPoint> { mContext = context; networkId = config.networkId; mConfig = config; + mPasspointUniqueId = config.getKey(); mFqdn = config.FQDN; setScanResultsPasspoint(homeScans, roamingScans); updateKey(); @@ -405,7 +423,7 @@ public class AccessPoint implements Comparable<AccessPoint> { if (isPasspoint()) { mKey = getKey(mConfig); } else if (isPasspointConfig()) { - mKey = getKey(mFqdn); + mKey = getKey(mPasspointUniqueId); } else if (isOsuProvider()) { mKey = getKey(mOsuProvider); } else { // Non-Passpoint AP @@ -447,9 +465,10 @@ public class AccessPoint implements Comparable<AccessPoint> { return other.getSpeed() - getSpeed(); } + WifiManager wifiManager = getWifiManager(); // Sort by signal strength, bucketed by level - int difference = WifiManager.calculateSignalLevel(other.mRssi, SIGNAL_LEVELS) - - WifiManager.calculateSignalLevel(mRssi, SIGNAL_LEVELS); + int difference = wifiManager.calculateSignalLevel(other.mRssi) + - wifiManager.calculateSignalLevel(mRssi); if (difference != 0) { return difference; } @@ -654,7 +673,7 @@ public class AccessPoint implements Comparable<AccessPoint> { } } } - return oldMetering == mIsScoredNetworkMetered; + return oldMetering != mIsScoredNetworkMetered; } /** @@ -674,19 +693,19 @@ public class AccessPoint implements Comparable<AccessPoint> { */ public static String getKey(WifiConfiguration config) { if (config.isPasspoint()) { - return getKey(config.FQDN); + return getKey(config.getKey()); } else { return getKey(removeDoubleQuotes(config.SSID), config.BSSID, getSecurity(config)); } } /** - * Returns the AccessPoint key corresponding to a Passpoint network by its FQDN. + * Returns the AccessPoint key corresponding to a Passpoint network by its unique identifier. */ - public static String getKey(String fqdn) { + public static String getKey(String passpointUniqueId) { return new StringBuilder() - .append(KEY_PREFIX_FQDN) - .append(fqdn).toString(); + .append(KEY_PREFIX_PASSPOINT_UNIQUE_ID) + .append(passpointUniqueId).toString(); } /** @@ -763,7 +782,7 @@ public class AccessPoint implements Comparable<AccessPoint> { public boolean matches(WifiConfiguration config) { if (config.isPasspoint()) { - return (isPasspoint() && config.FQDN.equals(mConfig.FQDN)); + return (isPasspoint() && config.getKey().equals(mConfig.getKey())); } if (!ssid.equals(removeDoubleQuotes(config.SSID)) @@ -863,13 +882,14 @@ public class AccessPoint implements Comparable<AccessPoint> { } /** - * Returns the number of levels to show for a Wifi icon, from 0 to {@link #SIGNAL_LEVELS}-1. + * Returns the number of levels to show for a Wifi icon, from 0 to + * {@link WifiManager#getMaxSignalLevel()}. * - * <p>Use {@#isReachable()} to determine if an AccessPoint is in range, as this method will + * <p>Use {@link #isReachable()} to determine if an AccessPoint is in range, as this method will * always return at least 0. */ public int getLevel() { - return WifiManager.calculateSignalLevel(mRssi, SIGNAL_LEVELS); + return getWifiManager().calculateSignalLevel(mRssi); } public int getRssi() { @@ -939,10 +959,6 @@ public class AccessPoint implements Comparable<AccessPoint> { mIsPskSaeTransitionMode = AccessPoint.isPskSaeTransitionMode(bestResult); mIsOweTransitionMode = AccessPoint.isOweTransitionMode(bestResult); - - mIsCarrierAp = bestResult.isCarrierAp; - mCarrierApEapType = bestResult.carrierApEapType; - mCarrierName = bestResult.carrierName; } // Update the config SSID of a Passpoint network to that of the best RSSI if (isPasspoint()) { @@ -977,6 +993,10 @@ public class AccessPoint implements Comparable<AccessPoint> { return concise ? context.getString(R.string.wifi_security_short_psk_sae) : context.getString(R.string.wifi_security_psk_sae); } + if (mIsOweTransitionMode) { + return concise ? context.getString(R.string.wifi_security_short_none_owe) : + context.getString(R.string.wifi_security_none_owe); + } switch(security) { case SECURITY_EAP: @@ -1048,7 +1068,7 @@ public class AccessPoint implements Comparable<AccessPoint> { public String getConfigName() { if (mConfig != null && mConfig.isPasspoint()) { return mConfig.providerFriendlyName; - } else if (mFqdn != null) { + } else if (mPasspointUniqueId != null) { return mProviderFriendlyName; } else { return ssid; @@ -1063,18 +1083,6 @@ public class AccessPoint implements Comparable<AccessPoint> { return null; } - public boolean isCarrierAp() { - return mIsCarrierAp; - } - - public int getCarrierApEapType() { - return mCarrierApEapType; - } - - public String getCarrierName() { - return mCarrierName; - } - public String getSavedNetworkSummary() { WifiConfiguration config = mConfig; if (config != null) { @@ -1098,6 +1106,10 @@ public class AccessPoint implements Comparable<AccessPoint> { return mContext.getString(R.string.saved_network, appInfo.loadLabel(pm)); } } + + if (isPasspointConfigurationR1() && isExpired()) { + return mContext.getString(R.string.wifi_passpoint_expired); + } return ""; } @@ -1128,6 +1140,10 @@ public class AccessPoint implements Comparable<AccessPoint> { * Returns the summary for the AccessPoint. */ public String getSettingsSummary(boolean convertSavedAsDisconnected) { + if (isPasspointConfigurationR1() && isExpired()) { + return mContext.getString(R.string.wifi_passpoint_expired); + } + // Update to new summary StringBuilder summary = new StringBuilder(); @@ -1142,22 +1158,20 @@ public class AccessPoint implements Comparable<AccessPoint> { summary.append(mContext.getString(R.string.tap_to_sign_up)); } } else if (isActive()) { - if (getDetailedState() == DetailedState.CONNECTED && mIsCarrierAp) { - // This is the active connection on a carrier AP - summary.append(String.format(mContext.getString(R.string.connected_via_carrier), - mCarrierName)); - } else { - summary.append(getSummary(mContext, /* ssid */ null, getDetailedState(), - mInfo != null && mInfo.isEphemeral(), - mInfo != null ? mInfo.getNetworkSuggestionOrSpecifierPackageName() : null)); - } + summary.append(getSummary(mContext, /* ssid */ null, getDetailedState(), + mInfo != null && mInfo.isEphemeral(), + mInfo != null ? mInfo.getRequestingPackageName() : null)); } else { // not active if (mConfig != null && mConfig.hasNoInternetAccess()) { - int messageID = mConfig.getNetworkSelectionStatus().isNetworkPermanentlyDisabled() + int messageID = + mConfig.getNetworkSelectionStatus().getNetworkSelectionStatus() + == NETWORK_SELECTION_PERMANENTLY_DISABLED ? R.string.wifi_no_internet_no_reconnect : R.string.wifi_no_internet; summary.append(mContext.getString(messageID)); - } else if (mConfig != null && !mConfig.getNetworkSelectionStatus().isNetworkEnabled()) { + } else if (mConfig != null + && (mConfig.getNetworkSelectionStatus().getNetworkSelectionStatus() + != NETWORK_SELECTION_ENABLED)) { WifiConfiguration.NetworkSelectionStatus networkStatus = mConfig.getNetworkSelectionStatus(); switch (networkStatus.getNetworkSelectionDisableReason()) { @@ -1168,26 +1182,19 @@ public class AccessPoint implements Comparable<AccessPoint> { summary.append(mContext.getString(R.string.wifi_check_password_try_again)); break; case WifiConfiguration.NetworkSelectionStatus.DISABLED_DHCP_FAILURE: - case WifiConfiguration.NetworkSelectionStatus.DISABLED_DNS_FAILURE: summary.append(mContext.getString(R.string.wifi_disabled_network_failure)); break; case WifiConfiguration.NetworkSelectionStatus.DISABLED_ASSOCIATION_REJECTION: summary.append(mContext.getString(R.string.wifi_disabled_generic)); break; } - } else if (mConfig != null && mConfig.getNetworkSelectionStatus().isNotRecommended()) { - summary.append(mContext.getString( - R.string.wifi_disabled_by_recommendation_provider)); - } else if (mIsCarrierAp) { - summary.append(String.format(mContext.getString( - R.string.available_via_carrier), mCarrierName)); } else if (!isReachable()) { // Wifi out of range summary.append(mContext.getString(R.string.wifi_not_in_range)); } else { // In range, not disabled. if (mConfig != null) { // Is saved network // Last attempt to connect to this failed. Show reason why - switch (mConfig.recentFailure.getAssociationStatus()) { - case WifiConfiguration.RecentFailure.STATUS_AP_UNABLE_TO_HANDLE_NEW_STA: + switch (mConfig.getRecentFailureReason()) { + case WifiConfiguration.RECENT_FAILURE_AP_UNABLE_TO_HANDLE_NEW_STA: summary.append(mContext.getString( R.string.wifi_ap_unable_to_handle_new_sta)); break; @@ -1263,7 +1270,7 @@ public class AccessPoint implements Comparable<AccessPoint> { * Return true if this AccessPoint represents a Passpoint provider configuration. */ public boolean isPasspointConfig() { - return mFqdn != null && mConfig == null; + return mPasspointUniqueId != null && mConfig == null; } /** @@ -1274,6 +1281,30 @@ public class AccessPoint implements Comparable<AccessPoint> { } /** + * Return true if this AccessPoint is expired. + */ + public boolean isExpired() { + if (mSubscriptionExpirationTimeInMillis <= 0) { + // Expiration time not specified. + return false; + } else { + return System.currentTimeMillis() >= mSubscriptionExpirationTimeInMillis; + } + } + + public boolean isPasspointConfigurationR1() { + return mPasspointConfigurationVersion == PasspointConfigurationVersion.NO_OSU_PROVISIONED; + } + + /** + * Return true if {@link PasspointConfiguration#isOsuProvisioned} is true, this may refer to R2 + * or R3. + */ + public boolean isPasspointConfigurationOsuProvisioned() { + return mPasspointConfigurationVersion == PasspointConfigurationVersion.OSU_PROVISIONED; + } + + /** * Starts the OSU Provisioning flow. */ public void startOsuProvisioning(@Nullable WifiManager.ActionListener connectListener) { @@ -1295,8 +1326,12 @@ public class AccessPoint implements Comparable<AccessPoint> { if (info.isOsuAp() || mOsuStatus != null) { return (info.isOsuAp() && mOsuStatus != null); } else if (info.isPasspointAp() || isPasspoint()) { + // TODO: Use TextUtils.equals(info.getPasspointUniqueId(), mConfig.getKey()) when API + // is available return (info.isPasspointAp() && isPasspoint() - && TextUtils.equals(info.getPasspointFqdn(), mConfig.FQDN)); + && TextUtils.equals(info.getPasspointFqdn(), mConfig.FQDN) + && TextUtils.equals(info.getPasspointProviderFriendlyName(), + mConfig.providerFriendlyName)); } if (networkId != WifiConfiguration.INVALID_NETWORK_ID) { @@ -1328,7 +1363,7 @@ public class AccessPoint implements Comparable<AccessPoint> { * Can only be called for unsecured networks. */ public void generateOpenNetworkConfig() { - if ((security != SECURITY_NONE) && (security != SECURITY_OWE)) { + if (!isOpenNetwork()) { throw new IllegalStateException(); } if (mConfig != null) @@ -1336,11 +1371,11 @@ public class AccessPoint implements Comparable<AccessPoint> { mConfig = new WifiConfiguration(); mConfig.SSID = AccessPoint.convertToQuotedString(ssid); - if (security == SECURITY_NONE || !getWifiManager().isEasyConnectSupported()) { + if (security == SECURITY_NONE) { mConfig.allowedKeyManagement.set(KeyMgmt.NONE); } else { mConfig.allowedKeyManagement.set(KeyMgmt.OWE); - mConfig.requirePMF = true; + mConfig.requirePmf = true; } } @@ -1362,15 +1397,18 @@ public class AccessPoint implements Comparable<AccessPoint> { if (mNetworkInfo != null) { savedState.putParcelable(KEY_NETWORKINFO, mNetworkInfo); } + if (mPasspointUniqueId != null) { + savedState.putString(KEY_PASSPOINT_UNIQUE_ID, mPasspointUniqueId); + } if (mFqdn != null) { savedState.putString(KEY_FQDN, mFqdn); } if (mProviderFriendlyName != null) { savedState.putString(KEY_PROVIDER_FRIENDLY_NAME, mProviderFriendlyName); } - savedState.putBoolean(KEY_IS_CARRIER_AP, mIsCarrierAp); - savedState.putInt(KEY_CARRIER_AP_EAP_TYPE, mCarrierApEapType); - savedState.putString(KEY_CARRIER_NAME, mCarrierName); + savedState.putLong(KEY_SUBSCRIPTION_EXPIRATION_TIME_IN_MILLIS, + mSubscriptionExpirationTimeInMillis); + savedState.putInt(KEY_PASSPOINT_CONFIGURATION_VERSION, mPasspointConfigurationVersion); savedState.putBoolean(KEY_IS_PSK_SAE_TRANSITION_MODE, mIsPskSaeTransitionMode); savedState.putBoolean(KEY_IS_OWE_TRANSITION_MODE, mIsOweTransitionMode); } @@ -1629,13 +1667,8 @@ public class AccessPoint implements Comparable<AccessPoint> { final ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); if (state == DetailedState.CONNECTED) { - IWifiManager wifiManager = IWifiManager.Stub.asInterface( - ServiceManager.getService(Context.WIFI_SERVICE)); - NetworkCapabilities nc = null; - - try { - nc = cm.getNetworkCapabilities(wifiManager.getCurrentNetwork()); - } catch (RemoteException e) {} + WifiManager wifiManager = context.getSystemService(WifiManager.class); + NetworkCapabilities nc = cm.getNetworkCapabilities(wifiManager.getCurrentNetwork()); if (nc != null) { if (nc.hasCapability(nc.NET_CAPABILITY_CAPTIVE_PORTAL)) { @@ -1758,7 +1791,10 @@ public class AccessPoint implements Comparable<AccessPoint> { if (config.allowedKeyManagement.get(KeyMgmt.OWE)) { return SECURITY_OWE; } - return (config.wepKeys[0] != null) ? SECURITY_WEP : SECURITY_NONE; + return (config.wepTxKeyIndex >= 0 + && config.wepTxKeyIndex < config.wepKeys.length + && config.wepKeys[config.wepTxKeyIndex] != null) + ? SECURITY_WEP : SECURITY_NONE; } public static String securityToString(int security, int pskType) { @@ -1805,6 +1841,13 @@ public class AccessPoint implements Comparable<AccessPoint> { } /** + * Return true if this is an open network AccessPoint. + */ + public boolean isOpenNetwork() { + return security == SECURITY_NONE || security == SECURITY_OWE; + } + + /** * Callbacks relaying changes to the AccessPoint representation. * * <p>All methods are invoked on the Main Thread. @@ -1929,11 +1972,11 @@ public class AccessPoint implements Comparable<AccessPoint> { return; } - String fqdn = passpointConfig.getHomeSp().getFqdn(); + String uniqueId = passpointConfig.getUniqueId(); for (Pair<WifiConfiguration, Map<Integer, List<ScanResult>>> pairing : wifiManager.getAllMatchingWifiConfigs(wifiManager.getScanResults())) { WifiConfiguration config = pairing.first; - if (TextUtils.equals(config.FQDN, fqdn)) { + if (TextUtils.equals(config.getKey(), uniqueId)) { List<ScanResult> homeScans = pairing.second.get(WifiManager.PASSPOINT_HOME_NETWORK); List<ScanResult> roamingScans = diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/LongPressWifiEntryPreference.java b/packages/SettingsLib/src/com/android/settingslib/wifi/LongPressWifiEntryPreference.java new file mode 100644 index 000000000000..503d60c87bb9 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/LongPressWifiEntryPreference.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2019 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.wifi; + +import android.content.Context; + +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceViewHolder; + +import com.android.wifitrackerlib.WifiEntry; + +/** + * WifiEntryPreference that can be long pressed. + */ +public class LongPressWifiEntryPreference extends WifiEntryPreference { + + private final Fragment mFragment; + + public LongPressWifiEntryPreference(Context context, WifiEntry wifiEntry, Fragment fragment) { + super(context, wifiEntry); + mFragment = fragment; + } + + @Override + public void onBindViewHolder(final PreferenceViewHolder view) { + super.onBindViewHolder(view); + if (mFragment != null) { + view.itemView.setOnCreateContextMenuListener(mFragment); + view.itemView.setTag(this); + view.itemView.setLongClickable(true); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java b/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java index 17a73acb9bda..2fb2481ac117 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java @@ -22,6 +22,7 @@ import android.net.NetworkInfo; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; import android.os.Bundle; import android.os.Parcelable; @@ -56,8 +57,6 @@ public class TestAccessPointBuilder { private int mSecurity = AccessPoint.SECURITY_NONE; private WifiConfiguration mWifiConfig; private WifiInfo mWifiInfo; - private boolean mIsCarrierAp = false; - private String mCarrierName = null; Context mContext; private ArrayList<ScanResult> mScanResults; @@ -85,7 +84,7 @@ public class TestAccessPointBuilder { bundle.putParcelable(AccessPoint.KEY_NETWORKINFO, mNetworkInfo); bundle.putParcelable(AccessPoint.KEY_WIFIINFO, mWifiInfo); if (mFqdn != null) { - bundle.putString(AccessPoint.KEY_FQDN, mFqdn); + bundle.putString(AccessPoint.KEY_PASSPOINT_UNIQUE_ID, mFqdn); } if (mProviderFriendlyName != null) { bundle.putString(AccessPoint.KEY_PROVIDER_FRIENDLY_NAME, mProviderFriendlyName); @@ -99,10 +98,6 @@ public class TestAccessPointBuilder { } bundle.putInt(AccessPoint.KEY_SECURITY, mSecurity); bundle.putInt(AccessPoint.KEY_SPEED, mSpeed); - bundle.putBoolean(AccessPoint.KEY_IS_CARRIER_AP, mIsCarrierAp); - if (mCarrierName != null) { - bundle.putString(AccessPoint.KEY_CARRIER_NAME, mCarrierName); - } AccessPoint ap = new AccessPoint(mContext, bundle); ap.setRssi(mRssi); @@ -132,13 +127,15 @@ public class TestAccessPointBuilder { @Keep public TestAccessPointBuilder setLevel(int level) { // Reversal of WifiManager.calculateSignalLevels + WifiManager wifiManager = mContext.getSystemService(WifiManager.class); + int maxSignalLevel = wifiManager.getMaxSignalLevel(); if (level == 0) { mRssi = MIN_RSSI; - } else if (level >= AccessPoint.SIGNAL_LEVELS) { + } else if (level > maxSignalLevel) { mRssi = MAX_RSSI; } else { float inputRange = MAX_RSSI - MIN_RSSI; - float outputRange = AccessPoint.SIGNAL_LEVELS - 1; + float outputRange = maxSignalLevel; mRssi = (int) (level * inputRange / outputRange + MIN_RSSI); } return this; @@ -241,16 +238,6 @@ public class TestAccessPointBuilder { return this; } - public TestAccessPointBuilder setIsCarrierAp(boolean isCarrierAp) { - mIsCarrierAp = isCarrierAp; - return this; - } - - public TestAccessPointBuilder setCarrierName(String carrierName) { - mCarrierName = carrierName; - return this; - } - public TestAccessPointBuilder setScoredNetworkCache( ArrayList<TimestampedScoredNetwork> scoredNetworkCache) { mScoredNetworkCache = scoredNetworkCache; diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java new file mode 100644 index 000000000000..a53bc9f966d2 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2019 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.wifi; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.text.TextUtils; +import android.view.View; +import android.widget.ImageButton; +import android.widget.ImageView; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.android.settingslib.R; +import com.android.settingslib.Utils; +import com.android.wifitrackerlib.WifiEntry; + +/** + * Preference to display a WifiEntry in a wifi picker. + */ +public class WifiEntryPreference extends Preference implements WifiEntry.WifiEntryCallback, + View.OnClickListener { + + private static final int[] STATE_SECURED = { + R.attr.state_encrypted + }; + + private static final int[] FRICTION_ATTRS = { + R.attr.wifi_friction + }; + + // These values must be kept within [WifiEntry.WIFI_LEVEL_MIN, WifiEntry.WIFI_LEVEL_MAX] + private 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 + }; + + // StateListDrawable to display secured lock / metered "$" icon + @Nullable private final StateListDrawable mFrictionSld; + private final IconInjector mIconInjector; + private WifiEntry mWifiEntry; + private int mLevel = -1; + private CharSequence mContentDescription; + private OnButtonClickListener mOnButtonClickListener; + + public WifiEntryPreference(@NonNull Context context, @NonNull WifiEntry wifiEntry) { + this(context, wifiEntry, new IconInjector(context)); + } + + @VisibleForTesting + WifiEntryPreference(@NonNull Context context, @NonNull WifiEntry wifiEntry, + @NonNull IconInjector iconInjector) { + super(context); + + setLayoutResource(R.layout.preference_access_point); + setWidgetLayoutResource(R.layout.access_point_friction_widget); + mFrictionSld = getFrictionStateListDrawable(); + mWifiEntry = wifiEntry; + mWifiEntry.setListener(this); + mIconInjector = iconInjector; + refresh(); + } + + public WifiEntry getWifiEntry() { + return mWifiEntry; + } + + @Override + public void onBindViewHolder(final PreferenceViewHolder view) { + super.onBindViewHolder(view); + final Drawable drawable = getIcon(); + if (drawable != null) { + drawable.setLevel(mLevel); + } + + view.itemView.setContentDescription(mContentDescription); + + // Turn off divider + view.findViewById(R.id.two_target_divider).setVisibility(View.INVISIBLE); + + // Enable the icon button when the help string in this WifiEntry is not null. + final ImageButton imageButton = (ImageButton) view.findViewById(R.id.icon_button); + final ImageView frictionImageView = (ImageView) view.findViewById( + R.id.friction_icon); + if (mWifiEntry.getHelpUriString() != null + && mWifiEntry.getConnectedState() == WifiEntry.CONNECTED_STATE_DISCONNECTED) { + final Drawable drawablehelp = getDrawable(R.drawable.ic_help); + drawablehelp.setTintList( + Utils.getColorAttr(getContext(), android.R.attr.colorControlNormal)); + ((ImageView) imageButton).setImageDrawable(drawablehelp); + imageButton.setVisibility(View.VISIBLE); + imageButton.setOnClickListener(this); + imageButton.setContentDescription( + getContext().getText(R.string.help_label)); + + if (frictionImageView != null) { + frictionImageView.setVisibility(View.GONE); + } + } else { + imageButton.setVisibility(View.GONE); + + if (frictionImageView != null) { + frictionImageView.setVisibility(View.VISIBLE); + bindFrictionImage(frictionImageView); + } + } + } + + /** + * Updates the title and summary; may indirectly call notifyChanged(). + */ + public void refresh() { + setTitle(mWifiEntry.getTitle()); + final int level = mWifiEntry.getLevel(); + if (level != mLevel) { + mLevel = level; + updateIcon(mLevel); + notifyChanged(); + } + + setSummary(mWifiEntry.getSummary(false /* concise */)); + mContentDescription = buildContentDescription(); + } + + /** + * Indicates the state of the WifiEntry has changed and clients may retrieve updates through + * the WifiEntry getter methods. + */ + public void onUpdated() { + // TODO(b/70983952): Fill this method in + refresh(); + } + + /** + * Result of the connect request indicated by the WifiEntry.CONNECT_STATUS constants. + */ + public void onConnectResult(int status) { + // TODO(b/70983952): Fill this method in + } + + /** + * Result of the disconnect request indicated by the WifiEntry.DISCONNECT_STATUS constants. + */ + public void onDisconnectResult(int status) { + // TODO(b/70983952): Fill this method in + } + + /** + * Result of the forget request indicated by the WifiEntry.FORGET_STATUS constants. + */ + public void onForgetResult(int status) { + // TODO(b/70983952): Fill this method in + } + + /** + * Result of the sign-in request indecated by the WifiEntry.SIGNIN_STATUS constants + */ + public void onSignInResult(int status) { + // TODO(b/70983952): Fill this method in + } + + + private void updateIcon(int level) { + if (level == -1) { + setIcon(null); + return; + } + + final Drawable drawable = mIconInjector.getIcon(level); + if (drawable != null) { + drawable.setTintList(Utils.getColorAttr(getContext(), + android.R.attr.colorControlNormal)); + setIcon(drawable); + } else { + setIcon(null); + } + } + + @Nullable + private StateListDrawable getFrictionStateListDrawable() { + TypedArray frictionSld; + try { + frictionSld = getContext().getTheme().obtainStyledAttributes(FRICTION_ATTRS); + } catch (Resources.NotFoundException e) { + // Fallback for platforms that do not need friction icon resources. + frictionSld = null; + } + return frictionSld != null ? (StateListDrawable) frictionSld.getDrawable(0) : null; + } + + /** + * Binds the friction icon drawable using a StateListDrawable. + * + * <p>Friction icons will be rebound when notifyChange() is called, and therefore + * do not need to be managed in refresh()</p>. + */ + private void bindFrictionImage(ImageView frictionImageView) { + if (frictionImageView == null || mFrictionSld == null) { + return; + } + if ((mWifiEntry.getSecurity() != WifiEntry.SECURITY_NONE) + && (mWifiEntry.getSecurity() != WifiEntry.SECURITY_OWE)) { + mFrictionSld.setState(STATE_SECURED); + } + frictionImageView.setImageDrawable(mFrictionSld.getCurrent()); + } + + /** + * Helper method to generate content description string. + */ + @VisibleForTesting + CharSequence buildContentDescription() { + final Context context = getContext(); + + CharSequence contentDescription = getTitle(); + final CharSequence summary = getSummary(); + if (!TextUtils.isEmpty(summary)) { + contentDescription = TextUtils.concat(contentDescription, ",", summary); + } + int level = mWifiEntry.getLevel(); + if (level >= 0 && level < WIFI_CONNECTION_STRENGTH.length) { + contentDescription = TextUtils.concat(contentDescription, ",", + context.getString(WIFI_CONNECTION_STRENGTH[level])); + } + return TextUtils.concat(contentDescription, ",", + mWifiEntry.getSecurity() == WifiEntry.SECURITY_NONE + ? context.getString(R.string.accessibility_wifi_security_type_none) + : context.getString(R.string.accessibility_wifi_security_type_secured)); + } + + + static class IconInjector { + private final Context mContext; + + IconInjector(Context context) { + mContext = context; + } + + public Drawable getIcon(int level) { + return mContext.getDrawable(Utils.getWifiIconResource(level)); + } + } + + /** + * Set listeners, who want to listen the button client event. + */ + public void setOnButtonClickListener(OnButtonClickListener listener) { + mOnButtonClickListener = listener; + notifyChanged(); + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.icon_button) { + if (mOnButtonClickListener != null) { + mOnButtonClickListener.onButtonClick(this); + } + } + } + + /** + * Callback to inform the caller that the icon button is clicked. + */ + public interface OnButtonClickListener { + + /** + * Register to listen the button click event. + */ + void onButtonClick(WifiEntryPreference preference); + } + + private Drawable getDrawable(@DrawableRes int iconResId) { + Drawable buttonIcon = null; + + try { + buttonIcon = getContext().getDrawable(iconResId); + } catch (Resources.NotFoundException exception) { + // Do nothing + } + return buttonIcon; + } + +} diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiSavedConfigUtils.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiSavedConfigUtils.java index 19e38081fcad..65c7786235bf 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiSavedConfigUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiSavedConfigUtils.java @@ -65,5 +65,17 @@ public class WifiSavedConfigUtils { } return savedConfigs; } + + /** + * Returns the count of the saved configurations on the device, including both Wi-Fi networks + * and Passpoint profiles. + * + * @param context The application context + * @param wifiManager An instance of {@link WifiManager} + * @return count of saved Wi-Fi networks + */ + public static int getAllConfigsCount(Context context, WifiManager wifiManager) { + return getAllConfigs(context, wifiManager).size(); + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java index 8bd5f57f9b71..b7ae3dca5c16 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java @@ -17,6 +17,7 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; @@ -28,7 +29,6 @@ import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.net.wifi.WifiNetworkScoreCache; -import android.net.wifi.WifiSsid; import android.os.Handler; import android.os.Looper; import android.provider.Settings; @@ -37,7 +37,10 @@ import com.android.settingslib.R; import java.util.List; -public class WifiStatusTracker extends ConnectivityManager.NetworkCallback { +/** + * Track status of Wi-Fi for the Sys UI. + */ +public class WifiStatusTracker { private final Context mContext; private final WifiNetworkScoreCache mWifiNetworkScoreCache; private final WifiManager mWifiManager; @@ -56,8 +59,9 @@ public class WifiStatusTracker extends ConnectivityManager.NetworkCallback { .clearCapabilities() .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build(); - private final ConnectivityManager.NetworkCallback mNetworkCallback = new ConnectivityManager - .NetworkCallback() { + private final NetworkCallback mNetworkCallback = new NetworkCallback() { + // Note: onCapabilitiesChanged is guaranteed to be called "immediately" after onAvailable + // and onLinkPropertiesChanged. @Override public void onCapabilitiesChanged( Network network, NetworkCapabilities networkCapabilities) { @@ -65,10 +69,35 @@ public class WifiStatusTracker extends ConnectivityManager.NetworkCallback { mCallback.run(); } }; + private final NetworkCallback mDefaultNetworkCallback = new NetworkCallback() { + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { + // network is now the default network, and its capabilities are nc. + // This method will always be called immediately after the network becomes the + // default, in addition to any time the capabilities change while the network is + // the default. + mDefaultNetwork = network; + mDefaultNetworkCapabilities = nc; + updateStatusLabel(); + mCallback.run(); + } + @Override + public void onLost(Network network) { + // The system no longer has a default network. + mDefaultNetwork = null; + mDefaultNetworkCapabilities = null; + updateStatusLabel(); + mCallback.run(); + } + }; + private Network mDefaultNetwork = null; + private NetworkCapabilities mDefaultNetworkCapabilities = null; private final Runnable mCallback; private WifiInfo mWifiInfo; public boolean enabled; + public boolean isCaptivePortal; + public boolean isDefaultNetwork; public int state; public boolean connected; public String ssid; @@ -94,14 +123,45 @@ public class WifiStatusTracker extends ConnectivityManager.NetworkCallback { mWifiNetworkScoreCache.registerListener(mCacheListener); mConnectivityManager.registerNetworkCallback( mNetworkRequest, mNetworkCallback, mHandler); + mConnectivityManager.registerDefaultNetworkCallback(mDefaultNetworkCallback, mHandler); } else { mNetworkScoreManager.unregisterNetworkScoreCache(NetworkKey.TYPE_WIFI, mWifiNetworkScoreCache); mWifiNetworkScoreCache.unregisterListener(); mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); + mConnectivityManager.unregisterNetworkCallback(mDefaultNetworkCallback); } } + /** + * Fetches initial state as if a WifiManager.NETWORK_STATE_CHANGED_ACTION have been received. + * This replaces the dependency on the initial sticky broadcast. + */ + public void fetchInitialState() { + if (mWifiManager == null) { + return; + } + updateWifiState(); + final NetworkInfo networkInfo = + mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + connected = networkInfo != null && networkInfo.isConnected(); + mWifiInfo = null; + ssid = null; + if (connected) { + mWifiInfo = mWifiManager.getConnectionInfo(); + if (mWifiInfo != null) { + if (mWifiInfo.isPasspointAp() || mWifiInfo.isOsuAp()) { + ssid = mWifiInfo.getPasspointProviderFriendlyName(); + } else { + ssid = getValidSsid(mWifiInfo); + } + updateRssi(mWifiInfo.getRssi()); + maybeRequestNetworkScore(); + } + } + updateStatusLabel(); + } + public void handleBroadcast(Intent intent) { if (mWifiManager == null) { return; @@ -143,7 +203,7 @@ public class WifiStatusTracker extends ConnectivityManager.NetworkCallback { private void updateRssi(int newRssi) { rssi = newRssi; - level = WifiManager.calculateSignalLevel(rssi, WifiManager.RSSI_LEVELS); + level = mWifiManager.calculateSignalLevel(rssi); } private void maybeRequestNetworkScore() { @@ -154,11 +214,25 @@ public class WifiStatusTracker extends ConnectivityManager.NetworkCallback { } private void updateStatusLabel() { - final NetworkCapabilities networkCapabilities - = mConnectivityManager.getNetworkCapabilities(mWifiManager.getCurrentNetwork()); + if (mWifiManager == null) { + return; + } + NetworkCapabilities networkCapabilities; + final Network currentWifiNetwork = mWifiManager.getCurrentNetwork(); + if (currentWifiNetwork != null && currentWifiNetwork.equals(mDefaultNetwork)) { + // Wifi is connected and the default network. + isDefaultNetwork = true; + networkCapabilities = mDefaultNetworkCapabilities; + } else { + isDefaultNetwork = false; + networkCapabilities = mConnectivityManager.getNetworkCapabilities( + mWifiManager.getCurrentNetwork()); + } + isCaptivePortal = false; if (networkCapabilities != null) { if (networkCapabilities.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) { statusLabel = mContext.getString(R.string.wifi_status_sign_in_required); + isCaptivePortal = true; return; } else if (networkCapabilities.hasCapability(NET_CAPABILITY_PARTIAL_CONNECTIVITY)) { statusLabel = mContext.getString(R.string.wifi_limited_connection); @@ -189,7 +263,7 @@ public class WifiStatusTracker extends ConnectivityManager.NetworkCallback { private String getValidSsid(WifiInfo info) { String ssid = info.getSSID(); - if (ssid != null && !WifiSsid.NONE.equals(ssid)) { + if (ssid != null && !WifiManager.UNKNOWN_SSID.equals(ssid)) { return ssid; } // OK, it's not in the connectionInfo; we have to go hunting for it diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java index d107bc33aa86..bf5ab1c9951a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java @@ -77,7 +77,16 @@ import java.util.stream.Collectors; /** * Tracks saved or available wifi networks and their state. + * + * @deprecated WifiTracker/AccessPoint is no longer supported, and will be removed in a future + * release. Clients that need a dynamic list of available wifi networks should migrate to one of the + * newer tracker classes, + * {@link com.android.wifitrackerlib.WifiPickerTracker}, + * {@link com.android.wifitrackerlib.SavedNetworkTracker}, + * {@link com.android.wifitrackerlib.NetworkDetailsTracker}, + * in conjunction with {@link com.android.wifitrackerlib.WifiEntry} to represent each wifi network. */ +@Deprecated public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestroy { /** * Default maximum age in millis of cached scored networks in @@ -182,7 +191,7 @@ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestro filter.addAction(WifiManager.NETWORK_IDS_CHANGED_ACTION); filter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION); filter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION); - filter.addAction(WifiManager.LINK_CONFIGURATION_CHANGED_ACTION); + filter.addAction(WifiManager.ACTION_LINK_CONFIGURATION_CHANGED); filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); filter.addAction(WifiManager.RSSI_CHANGED_ACTION); @@ -226,7 +235,7 @@ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestro mConnectivityManager = connectivityManager; // check if verbose logging developer option has been turned on or off - sVerboseLogging = mWifiManager != null && (mWifiManager.getVerboseLoggingLevel() > 0); + sVerboseLogging = mWifiManager != null && mWifiManager.isVerboseLoggingEnabled(); mFilter = filter; @@ -518,8 +527,7 @@ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestro int networkId, final List<WifiConfiguration> configs) { if (configs != null) { for (WifiConfiguration config : configs) { - if (mLastInfo != null && networkId == config.networkId && - !(config.selfAdded && config.numAssociation == 0)) { + if (mLastInfo != null && networkId == config.networkId) { return config; } } @@ -607,7 +615,7 @@ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestro List<ScanResult> cachedScanResults = new ArrayList<>(mScanResultCache.values()); - // Add a unique Passpoint AccessPoint for each Passpoint profile's FQDN. + // Add a unique Passpoint AccessPoint for each Passpoint profile's unique identifier. accessPoints.addAll(updatePasspointAccessPoints( mWifiManager.getAllMatchingWifiConfigs(cachedScanResults), cachedAccessPoints)); @@ -883,7 +891,7 @@ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestro fetchScansAndConfigsAndUpdateAccessPoints(); } else if (WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals(action) - || WifiManager.LINK_CONFIGURATION_CHANGED_ACTION.equals(action)) { + || WifiManager.ACTION_LINK_CONFIGURATION_CHANGED.equals(action)) { fetchScansAndConfigsAndUpdateAccessPoints(); } else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) { // TODO(sghuman): Refactor these methods so they cannot result in duplicate @@ -892,9 +900,7 @@ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestro updateNetworkInfo(info); fetchScansAndConfigsAndUpdateAccessPoints(); } else if (WifiManager.RSSI_CHANGED_ACTION.equals(action)) { - NetworkInfo info = - mConnectivityManager.getNetworkInfo(mWifiManager.getCurrentNetwork()); - updateNetworkInfo(info); + updateNetworkInfo(/* networkInfo= */ null); } } }; @@ -940,7 +946,7 @@ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestro // We don't send a NetworkInfo object along with this message, because even if we // fetch one from ConnectivityManager, it might be older than the most recent // NetworkInfo message we got via a WIFI_STATE_CHANGED broadcast. - updateNetworkInfo(null); + updateNetworkInfo(/* networkInfo= */ null); } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java index 4e6c005457c0..0e6a60bf47c1 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java @@ -16,9 +16,13 @@ package com.android.settingslib.wifi; +import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLED; +import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.getMaxNetworkSelectionDisableReason; + import android.content.Context; import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiConfiguration.NetworkSelectionStatus; import android.net.wifi.WifiInfo; import android.os.SystemClock; @@ -30,6 +34,8 @@ import java.util.Map; public class WifiUtils { + private static final int INVALID_RSSI = -127; + public static String buildLoggingSummary(AccessPoint accessPoint, WifiConfiguration config) { final StringBuilder summary = new StringBuilder(); final WifiInfo info = accessPoint.getInfo(); @@ -39,7 +45,9 @@ public class WifiUtils { summary.append(" f=" + Integer.toString(info.getFrequency())); } summary.append(" " + getVisibilityStatus(accessPoint)); - if (config != null && !config.getNetworkSelectionStatus().isNetworkEnabled()) { + if (config != null + && (config.getNetworkSelectionStatus().getNetworkSelectionStatus() + != NETWORK_SELECTION_ENABLED)) { summary.append(" (" + config.getNetworkSelectionStatus().getNetworkStatusString()); if (config.getNetworkSelectionStatus().getDisableTime() > 0) { long now = System.currentTimeMillis(); @@ -56,15 +64,14 @@ public class WifiUtils { } if (config != null) { - WifiConfiguration.NetworkSelectionStatus networkStatus = - config.getNetworkSelectionStatus(); - for (int index = WifiConfiguration.NetworkSelectionStatus.NETWORK_SELECTION_ENABLE; - index < WifiConfiguration.NetworkSelectionStatus - .NETWORK_SELECTION_DISABLED_MAX; index++) { - if (networkStatus.getDisableReasonCounter(index) != 0) { - summary.append(" " + WifiConfiguration.NetworkSelectionStatus - .getNetworkDisableReasonString(index) + "=" - + networkStatus.getDisableReasonCounter(index)); + NetworkSelectionStatus networkStatus = config.getNetworkSelectionStatus(); + for (int reason = 0; reason <= getMaxNetworkSelectionDisableReason(); reason++) { + if (networkStatus.getDisableReasonCounter(reason) != 0) { + summary.append(" ") + .append(NetworkSelectionStatus + .getNetworkSelectionDisableReasonString(reason)) + .append("=") + .append(networkStatus.getDisableReasonCounter(reason)); } } } @@ -93,20 +100,21 @@ public class WifiUtils { if (bssid != null) { visibility.append(" ").append(bssid); } + visibility.append(" standard = ").append(info.getWifiStandard()); visibility.append(" rssi=").append(info.getRssi()); visibility.append(" "); - visibility.append(" score=").append(info.score); + visibility.append(" score=").append(info.getScore()); if (accessPoint.getSpeed() != AccessPoint.Speed.NONE) { visibility.append(" speed=").append(accessPoint.getSpeedLabel()); } - visibility.append(String.format(" tx=%.1f,", info.txSuccessRate)); - visibility.append(String.format("%.1f,", info.txRetriesRate)); - visibility.append(String.format("%.1f ", info.txBadRate)); - visibility.append(String.format("rx=%.1f", info.rxSuccessRate)); + visibility.append(String.format(" tx=%.1f,", info.getSuccessfulTxPacketsPerSecond())); + visibility.append(String.format("%.1f,", info.getRetriedTxPacketsPerSecond())); + visibility.append(String.format("%.1f ", info.getLostTxPacketsPerSecond())); + visibility.append(String.format("rx=%.1f", info.getSuccessfulRxPacketsPerSecond())); } - int maxRssi5 = WifiConfiguration.INVALID_RSSI; - int maxRssi24 = WifiConfiguration.INVALID_RSSI; + int maxRssi5 = INVALID_RSSI; + int maxRssi24 = INVALID_RSSI; final int maxDisplayedScans = 4; int num5 = 0; // number of scanned BSSID on 5GHz band int num24 = 0; // number of scanned BSSID on 2.4Ghz band |