From be7402b5bbefc1cfe5fdba1e28fca7e7ffcaa0e1 Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Fri, 13 Sep 2024 17:32:31 +0800 Subject: [PATCH 01/12] Fix spatial audio toggle string BUG: 343317785 Test: local tested Flag: com.android.settings.flags.enable_bluetooth_device_details_polish Change-Id: I1e0e1191682576051b010e4c0dc34809cd1654f9 --- res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index c1207cf75ad..26b60ec72ec 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7977,10 +7977,10 @@ Off - Off + Fixed - Off + Head Tracking From 939189bde8f3770737610f28f8b22971285131b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Wed, 11 Sep 2024 18:36:47 +0200 Subject: [PATCH 02/12] Refactor shortcut updating, and do it on BOOT_COMPLETE This CL shuffles quite a bit of code around, but the effective differences are: * Unified shortcut updating code (language switch & backup restoration). * Shortcuts are also updated on boot (flagged by MODES_UI which will need this). * Removed usage of (long obsolete) AsyncTask. A further CL will add some special-casing for the DND->Modes shortcut transition. Bug: 365545604 Test: atest com.android.settings.shortcut + manual (switch language, reboot) Flag: android.app.modes_ui Change-Id: I30450d13cb05008d2a71ed89d4781eb81e5532b9 --- AndroidManifest.xml | 8 ++ .../settings/backup/SettingsBackupHelper.java | 12 +- .../LocaleDragAndDropAdapter.java | 10 +- .../CreateShortcutPreferenceController.java | 114 ++--------------- .../android/settings/shortcut/Shortcuts.java | 118 ++++++++++++++++++ .../shortcut/ShortcutsUpdateReceiver.java | 53 ++++++++ ...sUpdateTask.java => ShortcutsUpdater.java} | 33 +++-- ...reateShortcutPreferenceControllerTest.java | 12 +- .../settings/shortcut/ShortcutsTest.java | 68 ++++++++++ ...askTest.java => ShortcutsUpdaterTest.java} | 31 +---- 10 files changed, 298 insertions(+), 161 deletions(-) create mode 100644 src/com/android/settings/shortcut/Shortcuts.java create mode 100644 src/com/android/settings/shortcut/ShortcutsUpdateReceiver.java rename src/com/android/settings/shortcut/{ShortcutsUpdateTask.java => ShortcutsUpdater.java} (65%) create mode 100644 tests/robotests/src/com/android/settings/shortcut/ShortcutsTest.java rename tests/robotests/src/com/android/settings/shortcut/{ShortcutsUpdateTaskTest.java => ShortcutsUpdaterTest.java} (69%) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 93a185ebda9..5352c7c7215 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -5335,6 +5335,14 @@ + + + + + + diff --git a/src/com/android/settings/backup/SettingsBackupHelper.java b/src/com/android/settings/backup/SettingsBackupHelper.java index 73760a42a44..5fdbb006b51 100644 --- a/src/com/android/settings/backup/SettingsBackupHelper.java +++ b/src/com/android/settings/backup/SettingsBackupHelper.java @@ -16,17 +16,19 @@ package com.android.settings.backup; - import android.app.backup.BackupAgentHelper; +import android.util.Log; import com.android.settings.flags.Flags; import com.android.settings.onboarding.OnboardingFeatureProvider; import com.android.settings.overlay.FeatureFactory; -import com.android.settings.shortcut.CreateShortcutPreferenceController; +import com.android.settings.shortcut.ShortcutsUpdater; import com.android.settingslib.datastore.BackupRestoreStorageManager; /** Backup agent for Settings APK */ public class SettingsBackupHelper extends BackupAgentHelper { + private static final String TAG = "SettingsBackupHelper"; + public static final String SOUND_BACKUP_HELPER = "SoundSettingsBackup"; public static final String ACCESSIBILITY_APPEARANCE_BACKUP_HELPER = "AccessibilityAppearanceSettingsBackup"; @@ -58,6 +60,10 @@ public class SettingsBackupHelper extends BackupAgentHelper { public void onRestoreFinished() { super.onRestoreFinished(); BackupRestoreStorageManager.getInstance(this).onRestoreFinished(); - CreateShortcutPreferenceController.updateRestoredShortcuts(this); + try { + ShortcutsUpdater.updatePinnedShortcuts(this); + } catch (Exception e) { + Log.e(TAG, "Error updating shortcuts after restoring backup", e); + } } } diff --git a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java index 8d60ef2df3a..f85c0ccb203 100644 --- a/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java +++ b/src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java @@ -16,6 +16,8 @@ package com.android.settings.localepicker; +import static com.google.common.base.Preconditions.checkNotNull; + import android.app.settings.SettingsEnums; import android.content.Context; import android.graphics.Canvas; @@ -41,7 +43,8 @@ import com.android.internal.app.LocalePicker; import com.android.internal.app.LocaleStore; import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; -import com.android.settings.shortcut.ShortcutsUpdateTask; +import com.android.settings.shortcut.ShortcutsUpdater; +import com.android.settingslib.utils.ThreadUtils; import java.text.NumberFormat; import java.util.ArrayList; @@ -94,7 +97,7 @@ class LocaleDragAndDropAdapter LocaleDragAndDropAdapter(LocaleListEditor parent, List feedItemList) { mFeedItemList = feedItemList; mCacheItemList = new ArrayList<>(feedItemList); - mContext = parent.getContext(); + mContext = checkNotNull(parent.getContext()); final float dragElevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, mContext.getResources().getDisplayMetrics()); @@ -347,7 +350,8 @@ class LocaleDragAndDropAdapter LocalePicker.updateLocales(mLocalesToSetNext); mLocalesSetLast = mLocalesToSetNext; - new ShortcutsUpdateTask(mContext).execute(); + ThreadUtils.postOnBackgroundThread( + () -> ShortcutsUpdater.updatePinnedShortcuts(mContext)); mLocalesToSetNext = null; diff --git a/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java b/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java index 8f74bd9e05a..0e2e6bcb44c 100644 --- a/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java +++ b/src/com/android/settings/shortcut/CreateShortcutPreferenceController.java @@ -16,27 +16,19 @@ package com.android.settings.shortcut; +import static com.android.settings.shortcut.Shortcuts.SHORTCUT_PROBE; + import android.app.Activity; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.graphics.drawable.LayerDrawable; import android.net.ConnectivityManager; import android.util.Log; -import android.view.ContextThemeWrapper; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; @@ -48,7 +40,6 @@ import com.android.settings.Settings; import com.android.settings.Settings.DataUsageSummaryActivity; import com.android.settings.Settings.TetherSettingsActivity; import com.android.settings.Settings.WifiTetherSettingsActivity; -import com.android.settings.activityembedding.ActivityEmbeddingUtils; import com.android.settings.core.BasePreferenceController; import com.android.settings.gestures.OneHandedSettingsUtils; import com.android.settings.network.SubscriptionUtil; @@ -69,11 +60,6 @@ public class CreateShortcutPreferenceController extends BasePreferenceController private static final String TAG = "CreateShortcutPrefCtrl"; - static final String SHORTCUT_ID_PREFIX = "component-shortcut-"; - static final Intent SHORTCUT_PROBE = new Intent(Intent.ACTION_MAIN) - .addCategory("com.android.settings.SHORTCUT") - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - private final ShortcutManager mShortcutManager; private final PackageManager mPackageManager; private final ConnectivityManager mConnectivityManager; @@ -132,9 +118,7 @@ public class CreateShortcutPreferenceController extends BasePreferenceController if (mHost == null) { return false; } - final Intent shortcutIntent = createResultIntent( - buildShortcutIntent(uiContext, info), - info, clickTarget.getTitle()); + final Intent shortcutIntent = createResultIntent(info); mHost.setResult(Activity.RESULT_OK, shortcutIntent); logCreateShortcut(info); mHost.finish(); @@ -149,21 +133,20 @@ public class CreateShortcutPreferenceController extends BasePreferenceController * launcher widget using this intent. */ @VisibleForTesting - Intent createResultIntent(Intent shortcutIntent, ResolveInfo resolveInfo, - CharSequence label) { - ShortcutInfo info = createShortcutInfo(mContext, shortcutIntent, resolveInfo, label); + Intent createResultIntent(ResolveInfo resolveInfo) { + ShortcutInfo info = Shortcuts.createShortcutInfo(mContext, resolveInfo); Intent intent = mShortcutManager.createShortcutResultIntent(info); if (intent == null) { intent = new Intent(); } intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(mContext, R.mipmap.ic_launcher_settings)) - .putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent) - .putExtra(Intent.EXTRA_SHORTCUT_NAME, label); + .putExtra(Intent.EXTRA_SHORTCUT_INTENT, info.getIntent()) + .putExtra(Intent.EXTRA_SHORTCUT_NAME, info.getShortLabel()); final ActivityInfo activityInfo = resolveInfo.activityInfo; if (activityInfo.icon != 0) { - intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, createIcon( + intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, Shortcuts.createIcon( mContext, activityInfo.applicationInfo, activityInfo.icon, @@ -239,87 +222,6 @@ public class CreateShortcutPreferenceController extends BasePreferenceController info.activityInfo.name); } - private static Intent buildShortcutIntent(Context context, ResolveInfo info) { - Intent intent = new Intent(SHORTCUT_PROBE) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP) - .setClassName(info.activityInfo.packageName, info.activityInfo.name); - if (ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context)) { - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - } - return intent; - } - - private static ShortcutInfo createShortcutInfo(Context context, Intent shortcutIntent, - ResolveInfo resolveInfo, CharSequence label) { - final ActivityInfo activityInfo = resolveInfo.activityInfo; - - final Icon maskableIcon; - if (activityInfo.icon != 0 && activityInfo.applicationInfo != null) { - maskableIcon = Icon.createWithAdaptiveBitmap(createIcon( - context, - activityInfo.applicationInfo, activityInfo.icon, - R.layout.shortcut_badge_maskable, - context.getResources().getDimensionPixelSize(R.dimen.shortcut_size_maskable))); - } else { - maskableIcon = Icon.createWithResource(context, R.drawable.ic_launcher_settings); - } - final String shortcutId = SHORTCUT_ID_PREFIX + - shortcutIntent.getComponent().flattenToShortString(); - return new ShortcutInfo.Builder(context, shortcutId) - .setShortLabel(label) - .setIntent(shortcutIntent) - .setIcon(maskableIcon) - .build(); - } - - private static Bitmap createIcon(Context context, ApplicationInfo app, int resource, - int layoutRes, int size) { - final Context themedContext = new ContextThemeWrapper(context, - android.R.style.Theme_Material); - final View view = LayoutInflater.from(themedContext).inflate(layoutRes, null); - final int spec = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY); - view.measure(spec, spec); - final Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), - Bitmap.Config.ARGB_8888); - final Canvas canvas = new Canvas(bitmap); - - Drawable iconDrawable; - try { - iconDrawable = context.getPackageManager().getResourcesForApplication(app) - .getDrawable(resource, themedContext.getTheme()); - if (iconDrawable instanceof LayerDrawable) { - iconDrawable = ((LayerDrawable) iconDrawable).getDrawable(1); - } - ((ImageView) view.findViewById(android.R.id.icon)).setImageDrawable(iconDrawable); - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Cannot load icon from app " + app + ", returning a default icon"); - Icon icon = Icon.createWithResource(context, R.drawable.ic_launcher_settings); - ((ImageView) view.findViewById(android.R.id.icon)).setImageIcon(icon); - } - - view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); - view.draw(canvas); - return bitmap; - } - - public static void updateRestoredShortcuts(Context context) { - ShortcutManager sm = context.getSystemService(ShortcutManager.class); - List updatedShortcuts = new ArrayList<>(); - for (ShortcutInfo si : sm.getPinnedShortcuts()) { - if (si.getId().startsWith(SHORTCUT_ID_PREFIX)) { - ResolveInfo ri = context.getPackageManager().resolveActivity(si.getIntent(), 0); - - if (ri != null) { - updatedShortcuts.add(createShortcutInfo(context, - buildShortcutIntent(context, ri), ri, si.getShortLabel())); - } - } - } - if (!updatedShortcuts.isEmpty()) { - sm.updateShortcuts(updatedShortcuts); - } - } - private static final Comparator SHORTCUT_COMPARATOR = (i1, i2) -> i1.priority - i2.priority; } diff --git a/src/com/android/settings/shortcut/Shortcuts.java b/src/com/android/settings/shortcut/Shortcuts.java new file mode 100644 index 00000000000..53544ebe120 --- /dev/null +++ b/src/com/android/settings/shortcut/Shortcuts.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.shortcut; + +import static com.google.common.base.Preconditions.checkArgument; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.graphics.drawable.LayerDrawable; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; + +import com.android.settings.R; +import com.android.settings.activityembedding.ActivityEmbeddingUtils; + +class Shortcuts { + + private static final String TAG = "Shortcuts"; + + static final String SHORTCUT_ID_PREFIX = "component-shortcut-"; + static final Intent SHORTCUT_PROBE = new Intent(Intent.ACTION_MAIN) + .addCategory("com.android.settings.SHORTCUT") + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + static ShortcutInfo createShortcutInfo(Context context, ResolveInfo target) { + checkArgument(target.activityInfo != null); + String shortcutId = SHORTCUT_ID_PREFIX + + target.activityInfo.getComponentName().flattenToShortString(); + + return createShortcutInfo(context, shortcutId, target); + } + + static ShortcutInfo createShortcutInfo(Context context, String id, ResolveInfo target) { + Intent intent = new Intent(SHORTCUT_PROBE) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP) + .setClassName(target.activityInfo.packageName, target.activityInfo.name); + if (ActivityEmbeddingUtils.isEmbeddingActivityEnabled(context)) { + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + } + + CharSequence label = target.loadLabel(context.getPackageManager()); + Icon maskableIcon = getMaskableIcon(context, target.activityInfo); + + return new ShortcutInfo.Builder(context, id) + .setIntent(intent) + .setShortLabel(label) + .setIcon(maskableIcon) + .build(); + } + + private static Icon getMaskableIcon(Context context, ActivityInfo activityInfo) { + if (activityInfo.icon != 0 && activityInfo.applicationInfo != null) { + return Icon.createWithAdaptiveBitmap(createIcon( + context, + activityInfo.applicationInfo, activityInfo.icon, + R.layout.shortcut_badge_maskable, + context.getResources().getDimensionPixelSize(R.dimen.shortcut_size_maskable))); + } else { + return Icon.createWithResource(context, R.drawable.ic_launcher_settings); + } + } + + static Bitmap createIcon(Context context, ApplicationInfo app, int resource, int layoutRes, + int size) { + final Context themedContext = new ContextThemeWrapper(context, + android.R.style.Theme_Material); + final View view = LayoutInflater.from(themedContext).inflate(layoutRes, null); + final int spec = View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY); + view.measure(spec, spec); + final Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), + Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + + Drawable iconDrawable; + try { + iconDrawable = context.getPackageManager().getResourcesForApplication(app) + .getDrawable(resource, themedContext.getTheme()); + if (iconDrawable instanceof LayerDrawable) { + iconDrawable = ((LayerDrawable) iconDrawable).getDrawable(1); + } + ((ImageView) view.findViewById(android.R.id.icon)).setImageDrawable(iconDrawable); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Cannot load icon from app " + app + ", returning a default icon"); + Icon icon = Icon.createWithResource(context, R.drawable.ic_launcher_settings); + ((ImageView) view.findViewById(android.R.id.icon)).setImageIcon(icon); + } + + view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); + view.draw(canvas); + return bitmap; + } +} diff --git a/src/com/android/settings/shortcut/ShortcutsUpdateReceiver.java b/src/com/android/settings/shortcut/ShortcutsUpdateReceiver.java new file mode 100644 index 00000000000..657af5ba027 --- /dev/null +++ b/src/com/android/settings/shortcut/ShortcutsUpdateReceiver.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.shortcut; + +import android.app.Flags; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.settingslib.utils.ThreadUtils; + +public class ShortcutsUpdateReceiver extends BroadcastReceiver { + + private static final String TAG = "ShortcutsUpdateReceiver"; + + @Override + public void onReceive(@NonNull Context context, @NonNull Intent intent) { + if (!Flags.modesApi() || !Flags.modesUi()) { + return; + } + + if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { + PendingResult pendingResult = goAsync(); + + ThreadUtils.getBackgroundExecutor().execute(() -> { + try { + ShortcutsUpdater.updatePinnedShortcuts(context); + } catch (Exception e) { + Log.e(TAG, "Error trying to update Settings shortcuts", e); + } finally { + pendingResult.finish(); + } + }); + } + } +} diff --git a/src/com/android/settings/shortcut/ShortcutsUpdateTask.java b/src/com/android/settings/shortcut/ShortcutsUpdater.java similarity index 65% rename from src/com/android/settings/shortcut/ShortcutsUpdateTask.java rename to src/com/android/settings/shortcut/ShortcutsUpdater.java index 54f7d1ceafe..74799989496 100644 --- a/src/com/android/settings/shortcut/ShortcutsUpdateTask.java +++ b/src/com/android/settings/shortcut/ShortcutsUpdater.java @@ -16,8 +16,10 @@ package com.android.settings.shortcut; -import static com.android.settings.shortcut.CreateShortcutPreferenceController.SHORTCUT_ID_PREFIX; -import static com.android.settings.shortcut.CreateShortcutPreferenceController.SHORTCUT_PROBE; +import static com.android.settings.shortcut.Shortcuts.SHORTCUT_ID_PREFIX; +import static com.android.settings.shortcut.Shortcuts.SHORTCUT_PROBE; + +import static com.google.common.base.Preconditions.checkNotNull; import android.content.ComponentName; import android.content.Context; @@ -26,23 +28,22 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; -import android.os.AsyncTask; import java.util.ArrayList; import java.util.List; -public class ShortcutsUpdateTask extends AsyncTask { +public class ShortcutsUpdater { - private final Context mContext; - - public ShortcutsUpdateTask(Context context) { - mContext = context; - } - - @Override - public Void doInBackground(Void... params) { - ShortcutManager sm = mContext.getSystemService(ShortcutManager.class); - PackageManager pm = mContext.getPackageManager(); + /** + * Update label, icon, and intent of pinned shortcuts to Settings subpages. + * + *

Should be called whenever any of those could have changed, such as after changing locale, + * restoring a backup from a different device, or when flags controlling available features + * may have flipped. + */ + public static void updatePinnedShortcuts(Context context) { + ShortcutManager sm = checkNotNull(context.getSystemService(ShortcutManager.class)); + PackageManager pm = context.getPackageManager(); List updates = new ArrayList<>(); for (ShortcutInfo info : sm.getPinnedShortcuts()) { @@ -55,12 +56,10 @@ public class ShortcutsUpdateTask extends AsyncTask { if (ri == null) { continue; } - updates.add(new ShortcutInfo.Builder(mContext, info.getId()) - .setShortLabel(ri.loadLabel(pm)).build()); + updates.add(Shortcuts.createShortcutInfo(context, info.getId(), ri)); } if (!updates.isEmpty()) { sm.updateShortcuts(updates); } - return null; } } diff --git a/tests/robotests/src/com/android/settings/shortcut/CreateShortcutPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/shortcut/CreateShortcutPreferenceControllerTest.java index 9727dd13848..8442a37873b 100644 --- a/tests/robotests/src/com/android/settings/shortcut/CreateShortcutPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/shortcut/CreateShortcutPreferenceControllerTest.java @@ -16,7 +16,7 @@ package com.android.settings.shortcut; -import static com.android.settings.shortcut.CreateShortcutPreferenceController.SHORTCUT_ID_PREFIX; +import static com.android.settings.shortcut.Shortcuts.SHORTCUT_ID_PREFIX; import static com.google.common.truth.Truth.assertThat; @@ -101,10 +101,10 @@ public class CreateShortcutPreferenceControllerTest { when(mShortcutManager.createShortcutResultIntent(any(ShortcutInfo.class))) .thenReturn(new Intent().putExtra("d1", "d2")); - final Intent intent = new Intent(CreateShortcutPreferenceController.SHORTCUT_PROBE) + final Intent intent = new Intent(Shortcuts.SHORTCUT_PROBE) .setClass(mContext, Settings.ManageApplicationsActivity.class); final ResolveInfo ri = mContext.getPackageManager().resolveActivity(intent, 0); - final Intent result = mController.createResultIntent(intent, ri, "mock"); + final Intent result = mController.createResultIntent(ri); assertThat(result.getStringExtra("d1")).isEqualTo("d2"); assertThat((Object) result.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT)).isNotNull(); @@ -131,7 +131,7 @@ public class CreateShortcutPreferenceControllerTest { ri2.activityInfo.applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM; mPackageManager.setResolveInfosForIntent( - new Intent(CreateShortcutPreferenceController.SHORTCUT_PROBE), + new Intent(Shortcuts.SHORTCUT_PROBE), Arrays.asList(ri1, ri2)); doReturn(false).when(mController).canShowWifiHotspot(); @@ -158,7 +158,7 @@ public class CreateShortcutPreferenceControllerTest { ri2.activityInfo.applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM; mPackageManager.setResolveInfosForIntent( - new Intent(CreateShortcutPreferenceController.SHORTCUT_PROBE), + new Intent(Shortcuts.SHORTCUT_PROBE), Arrays.asList(ri1, ri2)); doReturn(false).when(mController).canShowWifiHotspot(); @@ -276,7 +276,7 @@ public class CreateShortcutPreferenceControllerTest { ri.activityInfo.applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM; mPackageManager.setResolveInfosForIntent( - new Intent(CreateShortcutPreferenceController.SHORTCUT_PROBE), + new Intent(Shortcuts.SHORTCUT_PROBE), Arrays.asList(ri)); } } diff --git a/tests/robotests/src/com/android/settings/shortcut/ShortcutsTest.java b/tests/robotests/src/com/android/settings/shortcut/ShortcutsTest.java new file mode 100644 index 00000000000..a347ff9e0db --- /dev/null +++ b/tests/robotests/src/com/android/settings/shortcut/ShortcutsTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.shortcut; + +import static com.android.settings.shortcut.Shortcuts.SHORTCUT_PROBE; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; + +import com.android.settings.Settings; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class ShortcutsTest { + + private Context mContext; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.getApplication(); + } + + @Test + public void shortcutsUpdateTask() { + final Intent intent = new Intent(SHORTCUT_PROBE) + .setClass(mContext, Settings.ManageApplicationsActivity.class); + final ResolveInfo ri = mContext.getPackageManager().resolveActivity(intent, 0); + assertThat(ri).isNotNull(); + + ShortcutInfo shortcut = Shortcuts.createShortcutInfo(mContext, ri); + + assertThat(shortcut.getLabel()).isNotNull(); + assertThat(shortcut.getLabel().toString()).isEqualTo("App info"); + + assertThat(shortcut.getIntent()).isNotNull(); + assertThat(shortcut.getIntent().getAction()).isEqualTo(Intent.ACTION_MAIN); + assertThat(shortcut.getIntent().getCategories()).contains("com.android.settings.SHORTCUT"); + assertThat(shortcut.getIntent().getComponent()).isEqualTo( + new ComponentName(mContext, Settings.ManageApplicationsActivity.class)); + assertThat(shortcut.getIcon()).isNotNull(); + } +} diff --git a/tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdateTaskTest.java b/tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdaterTest.java similarity index 69% rename from tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdateTaskTest.java rename to tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdaterTest.java index 8352e7a9634..b157174ab1f 100644 --- a/tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdateTaskTest.java +++ b/tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdaterTest.java @@ -16,14 +16,12 @@ package com.android.settings.shortcut; -import static com.android.settings.shortcut.CreateShortcutPreferenceController.SHORTCUT_ID_PREFIX; +import static com.android.settings.shortcut.Shortcuts.SHORTCUT_ID_PREFIX; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -31,9 +29,6 @@ import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; @@ -48,17 +43,14 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.shadow.api.Shadow; -import org.robolectric.shadows.ShadowPackageManager; import java.util.Arrays; import java.util.List; @RunWith(RobolectricTestRunner.class) -public class ShortcutsUpdateTaskTest { +public class ShortcutsUpdaterTest { private Context mContext; - private ShadowPackageManager mPackageManager; @Mock private ShortcutManager mShortcutManager; @@ -69,27 +61,12 @@ public class ShortcutsUpdateTaskTest { public void setup() { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; - mPackageManager = Shadow.extract(mContext.getPackageManager()); } @Test public void shortcutsUpdateTask() { mContext = spy(RuntimeEnvironment.application); doReturn(mShortcutManager).when(mContext).getSystemService(eq(Context.SHORTCUT_SERVICE)); - final Intent shortcut1 = new Intent(CreateShortcutPreferenceController.SHORTCUT_PROBE) - .setComponent(new ComponentName( - mContext, Settings.ManageApplicationsActivity.class)); - final ResolveInfo ri1 = mock(ResolveInfo.class); - ri1.nonLocalizedLabel = "label1"; - - final Intent shortcut2 = new Intent(CreateShortcutPreferenceController.SHORTCUT_PROBE) - .setComponent(new ComponentName( - mContext, Settings.SoundSettingsActivity.class)); - final ResolveInfo ri2 = mock(ResolveInfo.class); - ri2.nonLocalizedLabel = "label2"; - - mPackageManager.addResolveInfoForIntent(shortcut1, ri1); - mPackageManager.addResolveInfoForIntent(shortcut2, ri2); final List pinnedShortcuts = Arrays.asList( makeShortcut("d1"), @@ -99,7 +76,7 @@ public class ShortcutsUpdateTaskTest { makeShortcut(Settings.SoundSettingsActivity.class)); when(mShortcutManager.getPinnedShortcuts()).thenReturn(pinnedShortcuts); - new ShortcutsUpdateTask(mContext).doInBackground(); + ShortcutsUpdater.updatePinnedShortcuts(mContext); verify(mShortcutManager, times(1)).updateShortcuts(mListCaptor.capture()); @@ -108,6 +85,8 @@ public class ShortcutsUpdateTaskTest { assertThat(updates).hasSize(2); assertThat(pinnedShortcuts.get(2).getId()).isEqualTo(updates.get(0).getId()); assertThat(pinnedShortcuts.get(4).getId()).isEqualTo(updates.get(1).getId()); + assertThat(updates.get(0).getShortLabel().toString()).isEqualTo("App info"); + assertThat(updates.get(1).getShortLabel().toString()).isEqualTo("Sound & vibration"); } private ShortcutInfo makeShortcut(Class className) { From 31e3f274d5819af2a60ea2e31732600b3f9f66ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Wed, 11 Sep 2024 19:25:31 +0200 Subject: [PATCH 03/12] Fix existing DND Settings shortcuts to point to Modes Fixes: 365545604 Test: atest ShortcutsUpdaterTest + manual (flag flip + reboot) Flag: android.app.modes_ui Change-Id: I28f7e3e69175e92611668fdfa655a817ffcc905e --- .../settings/shortcut/ShortcutsUpdater.java | 51 ++++++++++++---- .../shortcut/ShortcutsUpdaterTest.java | 60 +++++++++++++++++-- 2 files changed, 96 insertions(+), 15 deletions(-) diff --git a/src/com/android/settings/shortcut/ShortcutsUpdater.java b/src/com/android/settings/shortcut/ShortcutsUpdater.java index 74799989496..90a60fda379 100644 --- a/src/com/android/settings/shortcut/ShortcutsUpdater.java +++ b/src/com/android/settings/shortcut/ShortcutsUpdater.java @@ -21,6 +21,7 @@ import static com.android.settings.shortcut.Shortcuts.SHORTCUT_PROBE; import static com.google.common.base.Preconditions.checkNotNull; +import android.app.Flags; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -29,6 +30,11 @@ import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settings.Settings; + import java.util.ArrayList; import java.util.List; @@ -43,23 +49,48 @@ public class ShortcutsUpdater { */ public static void updatePinnedShortcuts(Context context) { ShortcutManager sm = checkNotNull(context.getSystemService(ShortcutManager.class)); - PackageManager pm = context.getPackageManager(); List updates = new ArrayList<>(); for (ShortcutInfo info : sm.getPinnedShortcuts()) { - if (!info.getId().startsWith(SHORTCUT_ID_PREFIX)) { - continue; + ResolveInfo resolvedActivity = resolveActivity(context, info); + if (resolvedActivity != null) { + // Id is preserved to update an existing shortcut, but the activity it opens might + // be different, according to maybeGetReplacingComponent. + updates.add(Shortcuts.createShortcutInfo(context, info.getId(), resolvedActivity)); } - ComponentName cn = ComponentName.unflattenFromString( - info.getId().substring(SHORTCUT_ID_PREFIX.length())); - ResolveInfo ri = pm.resolveActivity(new Intent(SHORTCUT_PROBE).setComponent(cn), 0); - if (ri == null) { - continue; - } - updates.add(Shortcuts.createShortcutInfo(context, info.getId(), ri)); } if (!updates.isEmpty()) { sm.updateShortcuts(updates); } } + + @Nullable + private static ResolveInfo resolveActivity(Context context, ShortcutInfo shortcut) { + if (!shortcut.getId().startsWith(SHORTCUT_ID_PREFIX)) { + return null; + } + + ComponentName cn = ComponentName.unflattenFromString( + shortcut.getId().substring(SHORTCUT_ID_PREFIX.length())); + if (cn == null) { + return null; + } + + // Check if the componentName is obsolete and has been replaced by a different one. + cn = maybeGetReplacingComponent(context, cn); + PackageManager pm = context.getPackageManager(); + return pm.resolveActivity(new Intent(SHORTCUT_PROBE).setComponent(cn), 0); + } + + @NonNull + private static ComponentName maybeGetReplacingComponent(Context context, ComponentName cn) { + // ZenModeSettingsActivity is replaced by ModesSettingsActivity and will be deleted + // soon (so we shouldn't use ZenModeSettingsActivity.class). + if (Flags.modesApi() && Flags.modesUi() + && cn.getClassName().endsWith("Settings$ZenModeSettingsActivity")) { + return new ComponentName(context, Settings.ModesSettingsActivity.class); + } + + return cn; + } } diff --git a/tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdaterTest.java b/tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdaterTest.java index b157174ab1f..5324ff50f43 100644 --- a/tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdaterTest.java +++ b/tests/robotests/src/com/android/settings/shortcut/ShortcutsUpdaterTest.java @@ -27,14 +27,19 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.Flags; import android.content.ComponentName; import android.content.Context; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import com.android.settings.Settings; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -52,6 +57,9 @@ public class ShortcutsUpdaterTest { private Context mContext; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private ShortcutManager mShortcutManager; @Captor @@ -60,14 +68,12 @@ public class ShortcutsUpdaterTest { @Before public void setup() { MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; + mContext = spy(RuntimeEnvironment.application); + doReturn(mShortcutManager).when(mContext).getSystemService(eq(Context.SHORTCUT_SERVICE)); } @Test - public void shortcutsUpdateTask() { - mContext = spy(RuntimeEnvironment.application); - doReturn(mShortcutManager).when(mContext).getSystemService(eq(Context.SHORTCUT_SERVICE)); - + public void updatePinnedShortcuts_updatesAllShortcuts() { final List pinnedShortcuts = Arrays.asList( makeShortcut("d1"), makeShortcut("d2"), @@ -89,6 +95,50 @@ public class ShortcutsUpdaterTest { assertThat(updates.get(1).getShortLabel().toString()).isEqualTo("Sound & vibration"); } + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void updatePinnedShortcuts_withModesFlag_replacesDndByModes() { + List shortcuts = List.of( + makeShortcut(Settings.ZenModeSettingsActivity.class)); + when(mShortcutManager.getPinnedShortcuts()).thenReturn(shortcuts); + + ShortcutsUpdater.updatePinnedShortcuts(mContext); + + verify(mShortcutManager, times(1)).updateShortcuts(mListCaptor.capture()); + final List updates = mListCaptor.getValue(); + assertThat(updates).hasSize(1); + + // Id hasn't changed, but intent and label has. + ComponentName zenCn = new ComponentName(mContext, Settings.ZenModeSettingsActivity.class); + ComponentName modesCn = new ComponentName(mContext, Settings.ModesSettingsActivity.class); + assertThat(updates.get(0).getId()).isEqualTo( + SHORTCUT_ID_PREFIX + zenCn.flattenToShortString()); + assertThat(updates.get(0).getIntent().getComponent()).isEqualTo(modesCn); + assertThat(updates.get(0).getShortLabel().toString()).isEqualTo("Modes"); + } + + @Test + @DisableFlags(Flags.FLAG_MODES_UI) + public void updatePinnedShortcuts_withoutModesFlag_leavesDndAlone() { + List shortcuts = List.of( + makeShortcut(Settings.ZenModeSettingsActivity.class)); + when(mShortcutManager.getPinnedShortcuts()).thenReturn(shortcuts); + + ShortcutsUpdater.updatePinnedShortcuts(mContext); + + verify(mShortcutManager, times(1)).updateShortcuts(mListCaptor.capture()); + final List updates = mListCaptor.getValue(); + assertThat(updates).hasSize(1); + + // Nothing has changed. + ComponentName zenCn = new ComponentName(mContext, Settings.ZenModeSettingsActivity.class); + assertThat(updates.get(0).getId()).isEqualTo( + SHORTCUT_ID_PREFIX + zenCn.flattenToShortString()); + assertThat(updates.get(0).getIntent().getComponent()).isEqualTo(zenCn); + assertThat(updates.get(0).getShortLabel().toString()).isEqualTo("Do Not Disturb"); + + } + private ShortcutInfo makeShortcut(Class className) { ComponentName cn = new ComponentName(mContext, className); return makeShortcut(SHORTCUT_ID_PREFIX + cn.flattenToShortString()); From 23bba426b566b26305fb7e09bfb839bcd62265bf Mon Sep 17 00:00:00 2001 From: yumeichen Date: Wed, 11 Sep 2024 12:50:28 +0000 Subject: [PATCH 04/12] Hide the Default Notification Sound if enabling customized vibration Flag:com.android.server.notification.notification_vibration_in_sound_uri Test: atest NotificationRingtonePreferenceControllerTest Bug: 358525376 Change-Id: Ic7517828df746700ceb6e0605a9d32066c7aed50 --- ...ificationRingtonePreferenceController.java | 9 ++++ ...ationRingtonePreferenceControllerTest.java | 46 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/notification/NotificationRingtonePreferenceController.java b/src/com/android/settings/notification/NotificationRingtonePreferenceController.java index 00f478f2d28..946b7837995 100644 --- a/src/com/android/settings/notification/NotificationRingtonePreferenceController.java +++ b/src/com/android/settings/notification/NotificationRingtonePreferenceController.java @@ -19,6 +19,7 @@ package com.android.settings.notification; import android.content.Context; import android.media.RingtoneManager; +import com.android.server.notification.Flags; import com.android.settings.R; public class NotificationRingtonePreferenceController extends RingtonePreferenceControllerBase { @@ -31,6 +32,9 @@ public class NotificationRingtonePreferenceController extends RingtonePreference @Override public boolean isAvailable() { + if (isVibrationInSoundUriEnabled()) { + return false; + } return mContext.getResources().getBoolean(R.bool.config_show_notification_ringtone); } @@ -43,4 +47,9 @@ public class NotificationRingtonePreferenceController extends RingtonePreference public int getRingtoneType() { return RingtoneManager.TYPE_NOTIFICATION; } + + private boolean isVibrationInSoundUriEnabled() { + return Flags.notificationVibrationInSoundUri() && mContext.getResources().getBoolean( + com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported); + } } diff --git a/tests/robotests/src/com/android/settings/notification/NotificationRingtonePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/NotificationRingtonePreferenceControllerTest.java index 1aecad51242..a04a14de57b 100644 --- a/tests/robotests/src/com/android/settings/notification/NotificationRingtonePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/NotificationRingtonePreferenceControllerTest.java @@ -18,35 +18,77 @@ package com.android.settings.notification; import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; import android.media.RingtoneManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import com.android.server.notification.Flags; +import com.android.settings.R; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) public class NotificationRingtonePreferenceControllerTest { private NotificationRingtonePreferenceController mController; + @Mock private Context mMockContext; + @Mock private Resources mMockResources; + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new NotificationRingtonePreferenceController(RuntimeEnvironment.application); + when(mMockContext.getResources()).thenReturn(mMockResources); + mController = new NotificationRingtonePreferenceController(mMockContext); } @Test + @DisableFlags(Flags.FLAG_NOTIFICATION_VIBRATION_IN_SOUND_URI) public void isAvailable_byDefault_isTrue() { + when(mMockResources + .getBoolean(com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported)) + .thenReturn(false); + when(mMockResources.getBoolean(R.bool.config_show_notification_ringtone)) + .thenReturn(true); + assertThat(mController.isAvailable()).isTrue(); } @Test @Config(qualifiers = "mcc999") + @DisableFlags(Flags.FLAG_NOTIFICATION_VIBRATION_IN_SOUND_URI) public void isAvailable_whenNotVisible_isFalse() { + when(mMockResources + .getBoolean(com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported)) + .thenReturn(false); + when(mMockResources.getBoolean(R.bool.config_show_notification_ringtone)) + .thenReturn(false); + + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_NOTIFICATION_VIBRATION_IN_SOUND_URI) + public void isAvailable_whenFlagsNotificationVibrationInSoundUri_isFalse() { + when(mMockResources + .getBoolean(com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported)) + .thenReturn(true); + when(mMockResources.getBoolean(R.bool.config_show_notification_ringtone)) + .thenReturn(true); + assertThat(mController.isAvailable()).isFalse(); } From 777a179bd69efbb81c66c03a0eff3b06aa1b0445 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Fri, 13 Sep 2024 16:52:25 +0800 Subject: [PATCH 05/12] Create MobileNetworkSummaryRepository For MobileNetworkSummaryController to use, so it no longer use MobileNetworkRepository. Fix: 366097262 Flag: EXEMPT refactor Test: manual - on Network & internet Test: atest MobileNetworkSummaryRepositoryTest Change-Id: I8a9d52af8e230fc407a4339c27f73ef79d512b24 --- res/xml/network_provider_internet.xml | 3 +- .../MobileNetworkSummaryController.java | 219 ------------ .../network/MobileNetworkSummaryController.kt | 121 +++++++ .../network/MobileNetworkSummaryRepository.kt | 66 ++++ .../network/NetworkDashboardFragment.java | 11 +- .../settings/spa/network/SimsSection.kt | 2 +- .../MobileNetworkSummaryControllerTest.java | 333 ------------------ .../MobileNetworkSummaryControllerTest.kt | 151 ++++++++ .../MobileNetworkSummaryRepositoryTest.kt | 101 ++++++ 9 files changed, 445 insertions(+), 562 deletions(-) delete mode 100644 src/com/android/settings/network/MobileNetworkSummaryController.java create mode 100644 src/com/android/settings/network/MobileNetworkSummaryController.kt create mode 100644 src/com/android/settings/network/MobileNetworkSummaryRepository.kt delete mode 100644 tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java create mode 100644 tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryControllerTest.kt create mode 100644 tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryRepositoryTest.kt diff --git a/res/xml/network_provider_internet.xml b/res/xml/network_provider_internet.xml index e4ebe788b0c..292f1824552 100644 --- a/res/xml/network_provider_internet.xml +++ b/res/xml/network_provider_internet.xml @@ -52,9 +52,8 @@ android:order="-15" settings:keywords="@string/keywords_more_mobile_networks" settings:userRestriction="no_config_mobile_networks" - settings:isPreferenceVisible="@bool/config_show_sim_info" settings:useAdminDisabledSummary="true" - settings:searchable="@bool/config_show_sim_info"/> + settings:controller="com.android.settings.network.MobileNetworkSummaryController" /> mSubInfoEntityList; - private List mUiccInfoEntityList; - private List mMobileNetworkInfoEntityList; - private boolean mIsAirplaneModeOn; - private LifecycleOwner mLifecycleOwner; - - /** - * This controls the summary text and click behavior of the "Mobile network" item on the - * Network & internet page. There are 3 separate cases depending on the number of mobile network - * subscriptions: - *

    - *
  • No subscription: click action begins a UI flow to add a network subscription, and - * the summary text indicates this
  • - * - *
  • One subscription: click action takes you to details for that one network, and - * the summary text is the network name
  • - * - *
  • More than one subscription: click action takes you to a page listing the subscriptions, - * and the summary text gives the count of SIMs
  • - *
- */ - public MobileNetworkSummaryController(Context context, Lifecycle lifecycle, - LifecycleOwner lifecycleOwner) { - super(context); - mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); - mLifecycleOwner = lifecycleOwner; - mMobileNetworkRepository = MobileNetworkRepository.getInstance(context); - mIsAirplaneModeOn = mMobileNetworkRepository.isAirplaneModeOn(); - if (lifecycle != null) { - lifecycle.addObserver(this); - } - } - - @OnLifecycleEvent(ON_RESUME) - public void onResume() { - mMobileNetworkRepository.addRegister(mLifecycleOwner, this, - SubscriptionManager.INVALID_SUBSCRIPTION_ID); - mMobileNetworkRepository.updateEntity(); - } - - @OnLifecycleEvent(ON_PAUSE) - public void onPause() { - mMobileNetworkRepository.removeRegister(this); - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - mPreference = screen.findPreference(getPreferenceKey()); - } - - @Override - public CharSequence getSummary() { - - if ((mSubInfoEntityList == null || mSubInfoEntityList.isEmpty()) || ( - mUiccInfoEntityList == null || mUiccInfoEntityList.isEmpty()) || ( - mMobileNetworkInfoEntityList == null || mMobileNetworkInfoEntityList.isEmpty())) { - if (new EuiccRepository(mContext).showEuiccSettings()) { - return mContext.getResources().getString( - R.string.mobile_network_summary_add_a_network); - } - // set empty string to override previous text for carrier when SIM available - return ""; - } else if (mSubInfoEntityList.size() == 1) { - SubscriptionInfoEntity info = mSubInfoEntityList.get(0); - CharSequence displayName = info.uniqueName; - if (info.isEmbedded || mUiccInfoEntityList.get(0).isActive - || mMobileNetworkInfoEntityList.get(0).showToggleForPhysicalSim) { - return displayName; - } - return mContext.getString(R.string.mobile_network_tap_to_activate, displayName); - } else { - return mSubInfoEntityList.stream() - .map(SubscriptionInfoEntity::getUniqueDisplayName) - .collect(Collectors.joining(", ")); - } - } - - private void logPreferenceClick(Preference preference) { - mMetricsFeatureProvider.logClickedPreference(preference, - preference.getExtras().getInt(DashboardFragment.CATEGORY)); - } - - private void startAddSimFlow() { - final Intent intent = new Intent(EuiccManager.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION); - intent.setPackage(com.android.settings.Utils.PHONE_PACKAGE_NAME); - intent.putExtra(EuiccManager.EXTRA_FORCE_PROVISION, true); - mContext.startActivity(intent); - } - - private void initPreference() { - refreshSummary(mPreference); - mPreference.setOnPreferenceClickListener(null); - mPreference.setFragment(null); - mPreference.setEnabled(!mIsAirplaneModeOn); - } - - private void update() { - if (mPreference == null || mPreference.isDisabledByAdmin()) { - return; - } - - initPreference(); - if (((mSubInfoEntityList == null || mSubInfoEntityList.isEmpty()) - || (mUiccInfoEntityList == null || mUiccInfoEntityList.isEmpty()) - || (mMobileNetworkInfoEntityList == null - || mMobileNetworkInfoEntityList.isEmpty()))) { - if (new EuiccRepository(mContext).showEuiccSettings()) { - mPreference.setOnPreferenceClickListener((Preference pref) -> { - logPreferenceClick(pref); - startAddSimFlow(); - return true; - }); - } else { - mPreference.setEnabled(false); - } - return; - } - - mPreference.setFragment(MobileNetworkListFragment.class.getCanonicalName()); - } - - @Override - public boolean isAvailable() { - return new SimRepository(mContext).showMobileNetworkPage(); - } - - @Override - public String getPreferenceKey() { - return KEY; - } - - @Override - public void onAirplaneModeChanged(boolean airplaneModeEnabled) { - if (mIsAirplaneModeOn != airplaneModeEnabled) { - mIsAirplaneModeOn = airplaneModeEnabled; - update(); - } - } - - @Override - public void onAvailableSubInfoChanged(List subInfoEntityList) { - mSubInfoEntityList = subInfoEntityList; - update(); - } - - @Override - public void onAllUiccInfoChanged(List uiccInfoEntityList) { - mUiccInfoEntityList = uiccInfoEntityList; - update(); - } - - @Override - public void onAllMobileNetworkInfoChanged( - List mobileNetworkInfoEntityList) { - mMobileNetworkInfoEntityList = mobileNetworkInfoEntityList; - update(); - } -} diff --git a/src/com/android/settings/network/MobileNetworkSummaryController.kt b/src/com/android/settings/network/MobileNetworkSummaryController.kt new file mode 100644 index 00000000000..5980bbd7d05 --- /dev/null +++ b/src/com/android/settings/network/MobileNetworkSummaryController.kt @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network + +import android.content.Context +import android.provider.Settings +import androidx.lifecycle.LifecycleOwner +import androidx.preference.Preference +import androidx.preference.PreferenceScreen +import com.android.settings.R +import com.android.settings.core.BasePreferenceController +import com.android.settings.dashboard.DashboardFragment +import com.android.settings.network.telephony.SimRepository +import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.settings.spa.network.startAddSimFlow +import com.android.settingslib.RestrictedPreference +import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle +import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow +import kotlinx.coroutines.flow.Flow + +/** + * This controls the summary text and click behavior of the "Mobile network" item on the Network & + * internet page. There are 2 separate cases depending on the number of mobile network + * subscriptions: + * - No subscription: click action begins a UI flow to add a network subscription, and the summary + * text indicates this + * - Has subscriptions: click action takes you to a page listing the subscriptions, and the summary + * text gives the count of SIMs + */ +class MobileNetworkSummaryController +@JvmOverloads +constructor( + private val context: Context, + preferenceKey: String, + private val repository: MobileNetworkSummaryRepository = + MobileNetworkSummaryRepository(context), + private val airplaneModeOnFlow: Flow = + context.settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON), +) : BasePreferenceController(context, preferenceKey) { + private val metricsFeatureProvider = featureFactory.metricsFeatureProvider + private var preference: RestrictedPreference? = null + + private var isAirplaneModeOn = false + + override fun getAvailabilityStatus() = + if (SimRepository(mContext).showMobileNetworkPage()) AVAILABLE + else CONDITIONALLY_UNAVAILABLE + + override fun displayPreference(screen: PreferenceScreen) { + super.displayPreference(screen) + preference = screen.findPreference(preferenceKey) + } + + override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) { + repository + .subscriptionsStateFlow() + .collectLatestWithLifecycle(viewLifecycleOwner, action = ::update) + airplaneModeOnFlow.collectLatestWithLifecycle(viewLifecycleOwner) { + isAirplaneModeOn = it + updateEnabled() + } + } + + private fun update(state: MobileNetworkSummaryRepository.SubscriptionsState) { + val preference = preference ?: return + preference.onPreferenceClickListener = null + preference.fragment = null + when (state) { + MobileNetworkSummaryRepository.AddNetwork -> { + preference.summary = + context.getString(R.string.mobile_network_summary_add_a_network) + preference.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + logPreferenceClick() + startAddSimFlow(context) + true + } + } + + MobileNetworkSummaryRepository.NoSubscriptions -> { + preference.summary = null + } + + is MobileNetworkSummaryRepository.HasSubscriptions -> { + preference.summary = state.displayNames.joinToString(", ") + preference.fragment = MobileNetworkListFragment::class.java.canonicalName + } + } + updateEnabled() + } + + private fun updateEnabled() { + val preference = preference ?: return + if (preference.isDisabledByAdmin) return + preference.isEnabled = + (preference.onPreferenceClickListener != null || preference.fragment != null) && + !isAirplaneModeOn + } + + private fun logPreferenceClick() { + val preference = preference ?: return + metricsFeatureProvider.logClickedPreference( + preference, + preference.extras.getInt(DashboardFragment.CATEGORY), + ) + } +} diff --git a/src/com/android/settings/network/MobileNetworkSummaryRepository.kt b/src/com/android/settings/network/MobileNetworkSummaryRepository.kt new file mode 100644 index 00000000000..edf557bb725 --- /dev/null +++ b/src/com/android/settings/network/MobileNetworkSummaryRepository.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network + +import android.content.Context +import android.telephony.SubscriptionInfo +import com.android.settings.network.telephony.SubscriptionRepository +import com.android.settings.network.telephony.euicc.EuiccRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +class MobileNetworkSummaryRepository( + private val context: Context, + private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context), + private val euiccRepository: EuiccRepository = EuiccRepository(context), + private val getDisplayName: (SubscriptionInfo) -> String = { subInfo -> + SubscriptionUtil.getUniqueSubscriptionDisplayName(subInfo, context).toString() + }, +) { + sealed interface SubscriptionsState + + data object AddNetwork : SubscriptionsState + + data object NoSubscriptions : SubscriptionsState + + data class HasSubscriptions(val displayNames: List) : SubscriptionsState + + fun subscriptionsStateFlow(): Flow = + subDisplayNamesFlow() + .map { displayNames -> + if (displayNames.isEmpty()) { + if (euiccRepository.showEuiccSettings()) AddNetwork else NoSubscriptions + } else { + HasSubscriptions(displayNames) + } + } + .distinctUntilChanged() + .conflate() + .flowOn(Dispatchers.Default) + + private fun subDisplayNamesFlow(): Flow> = + subscriptionRepository + .selectableSubscriptionInfoListFlow() + .map { subInfos -> subInfos.map(getDisplayName) } + .distinctUntilChanged() + .conflate() + .flowOn(Dispatchers.Default) +} diff --git a/src/com/android/settings/network/NetworkDashboardFragment.java b/src/com/android/settings/network/NetworkDashboardFragment.java index aff91308e94..ee7d440bcf3 100644 --- a/src/com/android/settings/network/NetworkDashboardFragment.java +++ b/src/com/android/settings/network/NetworkDashboardFragment.java @@ -19,7 +19,7 @@ import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; -import androidx.lifecycle.LifecycleOwner; +import androidx.annotation.Nullable; import com.android.settings.R; import com.android.settings.SettingsDumpService; @@ -69,12 +69,11 @@ public class NetworkDashboardFragment extends DashboardFragment implements @Override protected List createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, getSettingsLifecycle(), - this /* LifecycleOwner */); + return buildPreferenceControllers(context, getSettingsLifecycle()); } private static List buildPreferenceControllers(Context context, - Lifecycle lifecycle, LifecycleOwner lifecycleOwner) { + @Nullable Lifecycle lifecycle) { final VpnPreferenceController vpnPreferenceController = new VpnPreferenceController(context); final PrivateDnsPreferenceController privateDnsPreferenceController = @@ -87,7 +86,6 @@ public class NetworkDashboardFragment extends DashboardFragment implements final List controllers = new ArrayList<>(); - controllers.add(new MobileNetworkSummaryController(context, lifecycle, lifecycleOwner)); controllers.add(vpnPreferenceController); controllers.add(privateDnsPreferenceController); @@ -114,8 +112,7 @@ public class NetworkDashboardFragment extends DashboardFragment implements @Override public List createPreferenceControllers(Context context) { - return buildPreferenceControllers(context, null /* lifecycle */, - null /* LifecycleOwner */); + return buildPreferenceControllers(context, null /* lifecycle */); } }; } diff --git a/src/com/android/settings/spa/network/SimsSection.kt b/src/com/android/settings/spa/network/SimsSection.kt index 276d121c24f..bd55b32a5e8 100644 --- a/src/com/android/settings/spa/network/SimsSection.kt +++ b/src/com/android/settings/spa/network/SimsSection.kt @@ -137,7 +137,7 @@ private fun AddSim() { } } -private fun startAddSimFlow(context: Context) { +fun startAddSimFlow(context: Context) { val intent = Intent(EuiccManager.ACTION_PROVISION_EMBEDDED_SUBSCRIPTION) intent.setPackage(Utils.PHONE_PACKAGE_NAME) intent.putExtra(EuiccManager.EXTRA_FORCE_PROVISION, true) diff --git a/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java b/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java deleted file mode 100644 index 1823d6d6bed..00000000000 --- a/tests/robotests/src/com/android/settings/network/MobileNetworkSummaryControllerTest.java +++ /dev/null @@ -1,333 +0,0 @@ -/* - * 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.settings.network; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.notNull; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.content.Intent; -import android.provider.Settings; -import android.telephony.SubscriptionInfo; -import android.telephony.SubscriptionManager; -import android.telephony.TelephonyManager; -import android.telephony.euicc.EuiccManager; -import android.text.TextUtils; - -import androidx.lifecycle.LifecycleOwner; -import androidx.preference.PreferenceScreen; - -import com.android.settings.Settings.MobileNetworkActivity; -import com.android.settings.widget.AddPreference; -import com.android.settingslib.RestrictedLockUtils; -import com.android.settingslib.core.lifecycle.Lifecycle; - -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; - -import java.util.Arrays; - -@RunWith(RobolectricTestRunner.class) -public class MobileNetworkSummaryControllerTest { - - @Mock - private TelephonyManager mTelephonyManager; - @Mock - private SubscriptionManager mSubscriptionManager; - @Mock - private EuiccManager mEuiccManager; - @Mock - private PreferenceScreen mPreferenceScreen; - @Mock - private MobileNetworkRepository mMobileNetworkRepository; - @Mock - private MobileNetworkRepository.MobileNetworkCallback mMobileNetworkCallback; - - private AddPreference mPreference; - private Context mContext; - private MobileNetworkSummaryController mController; - private LifecycleOwner mLifecycleOwner; - private Lifecycle mLifecycle; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mContext = spy(RuntimeEnvironment.application); - doReturn(mTelephonyManager).when(mContext).getSystemService(TelephonyManager.class); - doReturn(mSubscriptionManager).when(mContext).getSystemService(SubscriptionManager.class); - doReturn(mEuiccManager).when(mContext).getSystemService(EuiccManager.class); - mMobileNetworkRepository = MobileNetworkRepository.getInstance(mContext); - mLifecycleOwner = () -> mLifecycle; - mLifecycle = new Lifecycle(mLifecycleOwner); - mMobileNetworkRepository.addRegister(mLifecycleOwner, mMobileNetworkCallback, - SubscriptionManager.INVALID_SUBSCRIPTION_ID); - - when(mTelephonyManager.getNetworkCountryIso()).thenReturn(""); - when(mSubscriptionManager.isActiveSubscriptionId(anyInt())).thenReturn(true); - when(mEuiccManager.isEnabled()).thenReturn(true); - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.EUICC_PROVISIONED, 1); - - mController = new MobileNetworkSummaryController(mContext, mLifecycle, mLifecycleOwner); - mPreference = spy(new AddPreference(mContext, null)); - mPreference.setKey(mController.getPreferenceKey()); - when(mPreferenceScreen.findPreference(eq(mController.getPreferenceKey()))).thenReturn( - mPreference); - } - - @After - public void tearDown() { - mMobileNetworkRepository.removeRegister(mMobileNetworkCallback); - SubscriptionUtil.setActiveSubscriptionsForTesting(null); - SubscriptionUtil.setAvailableSubscriptionsForTesting(null); - } - - @Test - public void getSummary_noSubscriptions_returnSummaryCorrectly() { - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - assertThat(mController.getSummary()).isEqualTo("Add a network"); - } - - @Test - public void getSummary_noSubscriptionsNoEuiccMgr_correctSummaryAndClickHandler() { - when(mEuiccManager.isEnabled()).thenReturn(false); - assertThat(TextUtils.isEmpty(mController.getSummary())).isTrue(); - assertThat(mPreference.getOnPreferenceClickListener()).isNull(); - assertThat(mPreference.getFragment()).isNull(); - } - - @Test - @Ignore - public void getSummary_oneSubscription_correctSummaryAndClickHandler() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - when(sub1.getSubscriptionId()).thenReturn(1); - when(sub1.getDisplayName()).thenReturn("sub1"); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - SubscriptionUtil.setActiveSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - assertThat(mController.getSummary()).isEqualTo("sub1"); - assertThat(mPreference.getFragment()).isNull(); - final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); - doNothing().when(mContext).startActivity(intentCaptor.capture()); - mPreference.getOnPreferenceClickListener().onPreferenceClick(mPreference); - Intent intent = intentCaptor.getValue(); - assertThat(intent.getComponent().getClassName()).isEqualTo( - MobileNetworkActivity.class.getName()); - assertThat(intent.getIntExtra(Settings.EXTRA_SUB_ID, - SubscriptionManager.INVALID_SUBSCRIPTION_ID)).isEqualTo(sub1.getSubscriptionId()); - } - - @Test - @Ignore - public void getSummary_oneInactivePSim_cannotDisablePsim_correctSummaryAndClickHandler() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - when(sub1.getSubscriptionId()).thenReturn(1); - when(sub1.getDisplayName()).thenReturn("sub1"); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - when(mSubscriptionManager.isActiveSubscriptionId(eq(1))).thenReturn(false); - - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - assertThat(mController.getSummary()).isEqualTo("Tap to activate sub1"); - - assertThat(mPreference.getFragment()).isNull(); - mPreference.getOnPreferenceClickListener().onPreferenceClick(mPreference); - verify(mSubscriptionManager).setSubscriptionEnabled(eq(sub1.getSubscriptionId()), eq(true)); - } - - @Test - @Ignore - public void getSummary_oneInactivePSim_canDisablePsim_correctSummaryAndClickHandler() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - when(sub1.getSubscriptionId()).thenReturn(1); - when(sub1.getDisplayName()).thenReturn("sub1"); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - SubscriptionUtil.setActiveSubscriptionsForTesting(Arrays.asList(sub1)); - when(mSubscriptionManager.isActiveSubscriptionId(eq(1))).thenReturn(false); - when(mSubscriptionManager.canDisablePhysicalSubscription()).thenReturn(true); - - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - assertThat(mController.getSummary()).isEqualTo("sub1"); - - final ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); - doNothing().when(mContext).startActivity(intentCaptor.capture()); - mPreference.getOnPreferenceClickListener().onPreferenceClick(mPreference); - Intent intent = intentCaptor.getValue(); - assertThat(intent.getComponent().getClassName()).isEqualTo( - MobileNetworkActivity.class.getName()); - assertThat(intent.getIntExtra(Settings.EXTRA_SUB_ID, - SubscriptionManager.INVALID_SUBSCRIPTION_ID)).isEqualTo(sub1.getSubscriptionId()); - } - - @Test - public void addButton_noSubscriptionsNoEuiccMgr_noAddClickListener() { - when(mEuiccManager.isEnabled()).thenReturn(false); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - verify(mPreference, never()).setOnAddClickListener(notNull()); - } - - @Test - public void addButton_oneSubscriptionNoEuiccMgr_noAddClickListener() { - when(mEuiccManager.isEnabled()).thenReturn(false); - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - verify(mPreference, never()).setOnAddClickListener(notNull()); - } - - @Test - public void addButton_noSubscriptions_noAddClickListener() { - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - verify(mPreference, never()).setOnAddClickListener(notNull()); - } - - @Test - @Ignore - public void addButton_oneSubscription_hasAddClickListener() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - verify(mPreference).setOnAddClickListener(notNull()); - } - - @Test - @Ignore - public void addButton_twoSubscriptions_hasAddClickListener() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - final SubscriptionInfo sub2 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1, sub2)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - verify(mPreference).setOnAddClickListener(notNull()); - } - - @Test - @Ignore - public void addButton_oneSubscriptionAirplaneModeTurnedOn_addButtonGetsDisabled() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1); - mController.onAirplaneModeChanged(true); - - final ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class); - verify(mPreference, atLeastOnce()).setAddWidgetEnabled(captor.capture()); - assertThat(captor.getValue()).isFalse(); - } - - @Test - @Ignore - public void onResume_oneSubscriptionAirplaneMode_isDisabled() { - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1); - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - assertThat(mPreference.isEnabled()).isFalse(); - - final ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class); - verify(mPreference, atLeastOnce()).setAddWidgetEnabled(captor.capture()); - assertThat(captor.getValue()).isFalse(); - } - - @Test - public void onAvailableSubInfoChanged_noSubscriptionEsimDisabled_isDisabled() { - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0); - when(mEuiccManager.isEnabled()).thenReturn(false); - mController.displayPreference(mPreferenceScreen); - - mController.onAvailableSubInfoChanged(null); - - assertThat(mPreference.isEnabled()).isFalse(); - } - - @Test - public void onAirplaneModeChanged_oneSubscriptionAirplaneModeGetsTurnedOn_isDisabled() { - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - assertThat(mPreference.isEnabled()).isTrue(); - - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1); - mController.onAirplaneModeChanged(true); - - assertThat(mPreference.isEnabled()).isFalse(); - } - - @Test - @Ignore - public void onAirplaneModeChanged_oneSubscriptionAirplaneModeGetsTurnedOff_isEnabled() { - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1); - final SubscriptionInfo sub1 = mock(SubscriptionInfo.class); - SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1)); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - - assertThat(mPreference.isEnabled()).isFalse(); - - Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0); - mController.onAirplaneModeChanged(false); - - assertThat(mPreference.isEnabled()).isTrue(); - - final ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class); - verify(mPreference, atLeastOnce()).setAddWidgetEnabled(eq(false)); - verify(mPreference, atLeastOnce()).setAddWidgetEnabled(captor.capture()); - assertThat(captor.getValue()).isTrue(); - } - - @Test - public void onResume_disabledByAdmin_prefStaysDisabled() { - mPreference.setDisabledByAdmin(new RestrictedLockUtils.EnforcedAdmin()); - mController.displayPreference(mPreferenceScreen); - mController.onResume(); - verify(mPreference, never()).setEnabled(eq(true)); - } -} diff --git a/tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryControllerTest.kt b/tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryControllerTest.kt new file mode 100644 index 00000000000..69fa9c42e6c --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryControllerTest.kt @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network + +import android.content.Context +import androidx.lifecycle.testing.TestLifecycleOwner +import androidx.preference.PreferenceManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settingslib.RestrictedPreference +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class MobileNetworkSummaryControllerTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + private val preference = RestrictedPreference(context).apply { key = KEY } + private val preferenceScreen = PreferenceManager(context).createPreferenceScreen(context) + + private val mockMobileNetworkSummaryRepository = mock() + private val airplaneModeOnFlow = MutableStateFlow(false) + + private val controller = + MobileNetworkSummaryController( + context = context, + preferenceKey = KEY, + repository = mockMobileNetworkSummaryRepository, + airplaneModeOnFlow = airplaneModeOnFlow, + ) + + @Before + fun setUp() { + preferenceScreen.addPreference(preference) + controller.displayPreference(preferenceScreen) + } + + @Test + fun onViewCreated_noSubscriptions(): Unit = runBlocking { + mockMobileNetworkSummaryRepository.stub { + on { subscriptionsStateFlow() } doReturn + flowOf(MobileNetworkSummaryRepository.NoSubscriptions) + } + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.summary).isNull() + assertThat(preference.isEnabled).isFalse() + assertThat(preference.onPreferenceClickListener).isNull() + } + + @Test + fun onViewCreated_addNetwork(): Unit = runBlocking { + mockMobileNetworkSummaryRepository.stub { + on { subscriptionsStateFlow() } doReturn + flowOf(MobileNetworkSummaryRepository.AddNetwork) + } + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.summary) + .isEqualTo(context.getString(R.string.mobile_network_summary_add_a_network)) + assertThat(preference.isEnabled).isTrue() + assertThat(preference.onPreferenceClickListener).isNotNull() + } + + @Test + fun onViewCreated_hasSubscriptions(): Unit = runBlocking { + mockMobileNetworkSummaryRepository.stub { + on { subscriptionsStateFlow() } doReturn + flowOf( + MobileNetworkSummaryRepository.HasSubscriptions( + displayNames = listOf(DISPLAY_NAME_1, DISPLAY_NAME_2) + ) + ) + } + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.summary).isEqualTo("$DISPLAY_NAME_1, $DISPLAY_NAME_2") + assertThat(preference.isEnabled).isTrue() + assertThat(preference.fragment).isNotNull() + } + + @Test + fun onViewCreated_addNetworkAndAirplaneModeOn(): Unit = runBlocking { + mockMobileNetworkSummaryRepository.stub { + on { subscriptionsStateFlow() } doReturn + flowOf(MobileNetworkSummaryRepository.AddNetwork) + } + airplaneModeOnFlow.value = true + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.isEnabled).isFalse() + } + + @Test + fun onViewCreated_hasSubscriptionsAndAirplaneModeOn(): Unit = runBlocking { + mockMobileNetworkSummaryRepository.stub { + on { subscriptionsStateFlow() } doReturn + flowOf( + MobileNetworkSummaryRepository.HasSubscriptions( + displayNames = listOf(DISPLAY_NAME_1, DISPLAY_NAME_2) + ) + ) + } + airplaneModeOnFlow.value = true + + controller.onViewCreated(TestLifecycleOwner()) + delay(100) + + assertThat(preference.isEnabled).isFalse() + } + + + private companion object { + const val KEY = "test_key" + const val DISPLAY_NAME_1 = "Display Name 1" + const val DISPLAY_NAME_2 = "Display Name 2" + } +} diff --git a/tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryRepositoryTest.kt new file mode 100644 index 00000000000..463af96268f --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/MobileNetworkSummaryRepositoryTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network + +import android.content.Context +import android.telephony.SubscriptionInfo +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.network.telephony.SubscriptionRepository +import com.android.settings.network.telephony.euicc.EuiccRepository +import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class MobileNetworkSummaryRepositoryTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + private val mockSubscriptionRepository = mock() + private val mockEuiccRepository = mock() + + private val repository = + MobileNetworkSummaryRepository( + context = context, + subscriptionRepository = mockSubscriptionRepository, + euiccRepository = mockEuiccRepository, + getDisplayName = { it.displayName.toString() }, + ) + + @Test + fun subscriptionsStateFlow_noSubscriptionsAndShowEuicc_returnsAddNetwork() = runBlocking { + mockSubscriptionRepository.stub { + on { selectableSubscriptionInfoListFlow() } doReturn flowOf(emptyList()) + } + mockEuiccRepository.stub { on { showEuiccSettings() } doReturn true } + + val state = repository.subscriptionsStateFlow().firstWithTimeoutOrNull() + + assertThat(state).isEqualTo(MobileNetworkSummaryRepository.AddNetwork) + } + + @Test + fun subscriptionsStateFlow_noSubscriptionsAndHideEuicc_returnsNoSubscriptions() = runBlocking { + mockSubscriptionRepository.stub { + on { selectableSubscriptionInfoListFlow() } doReturn flowOf(emptyList()) + } + mockEuiccRepository.stub { on { showEuiccSettings() } doReturn false } + + val state = repository.subscriptionsStateFlow().firstWithTimeoutOrNull() + + assertThat(state).isEqualTo(MobileNetworkSummaryRepository.NoSubscriptions) + } + + @Test + fun subscriptionsStateFlow_hasSubscriptions_returnsHasSubscriptions() = runBlocking { + mockSubscriptionRepository.stub { + on { selectableSubscriptionInfoListFlow() } doReturn + flowOf( + listOf( + SubscriptionInfo.Builder().setDisplayName(DISPLAY_NAME_1).build(), + SubscriptionInfo.Builder().setDisplayName(DISPLAY_NAME_2).build(), + ) + ) + } + + val state = repository.subscriptionsStateFlow().firstWithTimeoutOrNull() + + assertThat(state) + .isEqualTo( + MobileNetworkSummaryRepository.HasSubscriptions( + listOf(DISPLAY_NAME_1, DISPLAY_NAME_2) + ) + ) + } + + private companion object { + const val DISPLAY_NAME_1 = "Sub 1" + const val DISPLAY_NAME_2 = "Sub 2" + } +} From 87bb5a7a62e74c0619d2e084d88612390a3c0fc4 Mon Sep 17 00:00:00 2001 From: arunvoddu Date: Wed, 18 Sep 2024 06:39:04 +0000 Subject: [PATCH 06/12] Fixed satellite icon tint color in Satellite Messaging screen Flag: EXEMPT bugfix Bug: 363271994 Test: Verified Manually Change-Id: I4d950afcb89873ae7255601a4a6f35182f1599c1 --- res/drawable/ic_android_satellite_24px.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/drawable/ic_android_satellite_24px.xml b/res/drawable/ic_android_satellite_24px.xml index 15f28840069..b08f19bee02 100644 --- a/res/drawable/ic_android_satellite_24px.xml +++ b/res/drawable/ic_android_satellite_24px.xml @@ -3,7 +3,7 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" - android:tint="?attr/colorControlNormal"> + android:tint="?android:attr/colorControlNormal"> From 1586dc9f255df6dfd5e8e3c46abb3bf8e90e2357 Mon Sep 17 00:00:00 2001 From: Joshua McCloskey Date: Wed, 18 Sep 2024 17:53:37 +0000 Subject: [PATCH 07/12] Ensure udfps enroll view is visible Fixes: 360076856 Test: Verified transitioning from landscape -> rl landscape and view is present. Test: Verified transitioning from rl landscape -> landscape and view is present. Test: Verified all other rotations of device, and view is present. Flag: EXEMPT bugfix Change-Id: Id90760b9bf23d411bca0c70406a223d6bb3f63b7 --- .../biometrics/fingerprint/UdfpsEnrollEnrollingView.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollEnrollingView.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollEnrollingView.java index c798dff4be7..c28f9e0e761 100644 --- a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollEnrollingView.java +++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollEnrollingView.java @@ -93,6 +93,7 @@ public class UdfpsEnrollEnrollingView extends GlifLayout { } else if (mShouldUseReverseLandscape) { swapHeaderAndContent(); } + mUdfpsEnrollView.setVisibility(View.VISIBLE); setOnHoverListener(); } From 800f81c8322776781b2b6c8d6c17a8ff835612bb Mon Sep 17 00:00:00 2001 From: Yiyi Shen Date: Thu, 15 Aug 2024 16:50:52 +0800 Subject: [PATCH 08/12] [Audiosharing] Refine share then pair flow Currently when there is one active LEA headset and users toggle on the audio sharing, a dialog will pop up to ask users to pair new headset and share audio with it. After user click pair new device button on the dialog: 1. Route users to pair new device page. 2. If users pair an LEA headset, finish the pair new device page and auto add source to the headset with loading indicators on audio sharing page. 3. If users pair a classic headset, wait for timeout, pop up dialog saying the paired headset is not compatible for audio sharing and finish the pair new device page. Test: atest Flag: com.android.settingslib.flags.enable_le_audio_sharing Bug: 331892035 Change-Id: Ifb9579db0ef57d3a379cb5d17c66a604d1396bb4 --- .../BluetoothDevicePairingDetailBase.java | 249 +++++++++++++++++- .../AudioSharingDashboardFragment.java | 54 +++- .../AudioSharingDialogFragment.java | 48 ++-- ...udioSharingIncompatibleDialogFragment.java | 5 +- ...udioSharingLoadingStateDialogFragment.java | 24 +- .../AudioSharingSwitchBarController.java | 35 ++- .../BluetoothDevicePairingDetailBaseTest.java | 175 +++++++++++- .../AudioSharingDashboardFragmentTest.java | 93 ++++++- .../AudioSharingDialogFragmentTest.java | 38 ++- ...SharingIncompatibleDialogFragmentTest.java | 13 +- .../AudioSharingSwitchBarControllerTest.java | 13 + 11 files changed, 672 insertions(+), 75 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java index d71328eed3e..86f090e8be5 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java @@ -18,32 +18,94 @@ package com.android.settings.bluetooth; import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING; + +import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Intent; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; import android.util.Log; +import android.view.LayoutInflater; import android.view.View; +import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.accessibility.AccessibilityStatsLogUtils; +import com.android.settings.connecteddevice.audiosharing.AudioSharingIncompatibleDialogFragment; import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HearingAidStatsLogUtils; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; /** * Abstract class for providing basic interaction for a list of Bluetooth devices in bluetooth * device pairing detail page. */ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPreferenceFragment { + private static final long AUTO_DISMISS_TIME_THRESHOLD_MS = TimeUnit.SECONDS.toMillis(10); + private static final int AUTO_DISMISS_MESSAGE_ID = 1001; protected boolean mInitialScanStarted; @VisibleForTesting protected BluetoothProgressCategory mAvailableDevicesCategory; + @Nullable + private volatile BluetoothDevice mJustBonded = null; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); + @Nullable + private AlertDialog mLoadingDialog = null; + @VisibleForTesting + boolean mShouldTriggerAudioSharingShareThenPairFlow = false; + private CopyOnWriteArrayList mDevicesWithMetadataChangedListener = + new CopyOnWriteArrayList<>(); + + // BluetoothDevicePreference updates the summary based on several callbacks, including + // BluetoothAdapter.OnMetadataChangedListener and BluetoothCallback. In most cases, + // metadata changes callback will be triggered before onDeviceBondStateChanged(BOND_BONDED). + // And before we hear onDeviceBondStateChanged(BOND_BONDED), the BluetoothDevice.getState() has + // already been BOND_BONDED. These event sequence will lead to: before we hear + // onDeviceBondStateChanged(BOND_BONDED), BluetoothDevicePreference's summary has already + // change from "Pairing..." to empty since it listens to metadata changes happens earlier. + // + // In share then pair flow, we have to wait on this page till the device is connected. + // The BluetoothDevicePreference summary will be blank for seconds between "Pairing..." and + // "Connecting..." To help users better understand the process, we listen to metadata change + // as well and show a loading dialog with "Connecting to ...." once BluetoothDevice.getState() + // gets to BOND_BONDED. + final BluetoothAdapter.OnMetadataChangedListener mMetadataListener = + new BluetoothAdapter.OnMetadataChangedListener() { + @Override + public void onMetadataChanged(@NonNull BluetoothDevice device, int key, + @Nullable byte[] value) { + Log.d(getLogTag(), "onMetadataChanged device = " + device + ", key = " + key); + if (mShouldTriggerAudioSharingShareThenPairFlow && mLoadingDialog == null + && device.getBondState() == BluetoothDevice.BOND_BONDED + && mSelectedList.contains(device)) { + triggerAudioSharingShareThenPairFlow(device); + // Once device is bonded, remove the listener + removeOnMetadataChangedListener(device); + } + } + }; public BluetoothDevicePairingDetailBase() { super(DISALLOW_CONFIG_BLUETOOTH); @@ -68,6 +130,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere return; } updateBluetooth(); + mShouldTriggerAudioSharingShareThenPairFlow = shouldTriggerAudioSharingShareThenPairFlow(); } @Override @@ -80,6 +143,26 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere disableScanning(); } + @Override + public void onDestroy() { + super.onDestroy(); + var unused = ThreadUtils.postOnBackgroundThread(() -> { + mDevicesWithMetadataChangedListener.forEach( + device -> { + try { + if (mBluetoothAdapter != null) { + mBluetoothAdapter.removeOnMetadataChangedListener(device, + mMetadataListener); + mDevicesWithMetadataChangedListener.remove(device); + } + } catch (IllegalArgumentException e) { + Log.d(getLogTag(), "Fail to remove listener: " + e); + } + }); + mDevicesWithMetadataChangedListener.clear(); + }); + } + @Override public void onBluetoothStateChanged(int bluetoothState) { super.onBluetoothStateChanged(bluetoothState); @@ -92,16 +175,37 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere @Override public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { if (bondState == BluetoothDevice.BOND_BONDED) { + if (cachedDevice != null && mShouldTriggerAudioSharingShareThenPairFlow) { + BluetoothDevice device = cachedDevice.getDevice(); + if (device != null && mSelectedList.contains(device)) { + triggerAudioSharingShareThenPairFlow(device); + removeOnMetadataChangedListener(device); + return; + } + } // If one device is connected(bonded), then close this fragment. finish(); return; } else if (bondState == BluetoothDevice.BOND_BONDING) { + if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) { + BluetoothDevice device = cachedDevice.getDevice(); + if (device != null && mSelectedList.contains(device)) { + addOnMetadataChangedListener(device); + } + } // Set the bond entry where binding process starts for logging hearing aid device info final int pageId = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider() .getAttribution(getActivity()); final int bondEntry = AccessibilityStatsLogUtils.convertToHearingAidInfoBondEntry( pageId); HearingAidStatsLogUtils.setBondEntryForDevice(bondEntry, cachedDevice); + } else if (bondState == BluetoothDevice.BOND_NONE) { + if (mShouldTriggerAudioSharingShareThenPairFlow && cachedDevice != null) { + BluetoothDevice device = cachedDevice.getDevice(); + if (device != null && mSelectedList.contains(device)) { + removeOnMetadataChangedListener(device); + } + } } if (mSelectedDevice != null && cachedDevice != null) { BluetoothDevice device = cachedDevice.getDevice(); @@ -114,7 +218,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere } @Override - public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, + public void onProfileConnectionStateChanged( + @NonNull CachedBluetoothDevice cachedDevice, @ConnectionState int state, int bluetoothProfile) { // This callback is used to handle the case that bonded device is connected in pairing list. // 1. If user selected multiple bonded devices in pairing list, after connected @@ -123,8 +228,22 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere // removed from paring list. if (cachedDevice != null && cachedDevice.isConnected()) { final BluetoothDevice device = cachedDevice.getDevice(); - if (device != null && mSelectedList.contains(device)) { - finish(); + if (device != null + && mSelectedList.contains(device)) { + if (!BluetoothUtils.isAudioSharingEnabled()) { + finish(); + return; + } + if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT + && state == BluetoothAdapter.STATE_CONNECTED + && device.equals(mJustBonded) + && mShouldTriggerAudioSharingShareThenPairFlow) { + Log.d(getLogTag(), + "onProfileConnectionStateChanged, assistant profile connected"); + dismissConnectingDialog(); + mHandler.removeMessages(AUTO_DISMISS_MESSAGE_ID); + finishFragmentWithResultForAudioSharing(device); + } } else { onDeviceDeleted(cachedDevice); } @@ -148,6 +267,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere public void onDevicePreferenceClick(BluetoothDevicePreference btPreference) { disableScanning(); super.onDevicePreferenceClick(btPreference); + // Clean up the previous bond value + mJustBonded = null; } @VisibleForTesting @@ -165,8 +286,8 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere * {@code bluetoothState} is off. * * @param bluetoothState the current Bluetooth state, the possible values that will handle here: - * {@link android.bluetooth.BluetoothAdapter#STATE_OFF}, - * {@link android.bluetooth.BluetoothAdapter#STATE_ON}, + * {@link android.bluetooth.BluetoothAdapter#STATE_OFF}, + * {@link android.bluetooth.BluetoothAdapter#STATE_ON}, */ @VisibleForTesting public void updateContent(int bluetoothState) { @@ -187,4 +308,122 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast, Toast.LENGTH_SHORT).show(); } + + @VisibleForTesting + boolean shouldTriggerAudioSharingShareThenPairFlow() { + if (!BluetoothUtils.isAudioSharingEnabled()) return false; + Activity activity = getActivity(); + Intent intent = activity == null ? null : activity.getIntent(); + Bundle args = + intent == null ? null : + intent.getBundleExtra( + SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS); + return args != null + && args.getBoolean(EXTRA_PAIR_AND_JOIN_SHARING, false); + } + + private void addOnMetadataChangedListener(@Nullable BluetoothDevice device) { + var unused = ThreadUtils.postOnBackgroundThread(() -> { + if (mBluetoothAdapter != null && device != null + && !mDevicesWithMetadataChangedListener.contains(device)) { + mBluetoothAdapter.addOnMetadataChangedListener(device, mExecutor, + mMetadataListener); + mDevicesWithMetadataChangedListener.add(device); + } + }); + } + + private void removeOnMetadataChangedListener(@Nullable BluetoothDevice device) { + var unused = ThreadUtils.postOnBackgroundThread(() -> { + if (mBluetoothAdapter != null && device != null + && mDevicesWithMetadataChangedListener.contains(device)) { + try { + mBluetoothAdapter.removeOnMetadataChangedListener(device, mMetadataListener); + mDevicesWithMetadataChangedListener.remove(device); + } catch (IllegalArgumentException e) { + Log.d(getLogTag(), "Fail to remove listener: " + e); + } + } + }); + } + + private void triggerAudioSharingShareThenPairFlow( + @NonNull BluetoothDevice device) { + var unused = ThreadUtils.postOnBackgroundThread(() -> { + if (mJustBonded != null) { + Log.d(getLogTag(), "Skip triggerAudioSharingShareThenPairFlow, already done"); + return; + } + mJustBonded = device; + // Show connecting device loading state + String aliasName = device.getAlias(); + String deviceName = TextUtils.isEmpty(aliasName) ? device.getAddress() + : aliasName; + showConnectingDialog("Connecting to " + deviceName + "..."); + // Wait for AUTO_DISMISS_TIME_THRESHOLD_MS and check if the paired device supports audio + // sharing. + if (!mHandler.hasMessages(AUTO_DISMISS_MESSAGE_ID)) { + mHandler.postDelayed(() -> + postOnMainThread( + () -> { + Log.d(getLogTag(), "Show incompatible dialog when timeout"); + dismissConnectingDialog(); + AudioSharingIncompatibleDialogFragment.show(this, deviceName, + () -> finish()); + }), AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); + } + }); + } + + private void finishFragmentWithResultForAudioSharing(@Nullable BluetoothDevice device) { + Intent resultIntent = new Intent(); + resultIntent.putExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, device); + if (getActivity() != null) { + getActivity().setResult(Activity.RESULT_OK, resultIntent); + } + finish(); + } + + // TODO: use DialogFragment + private void showConnectingDialog(@NonNull String message) { + postOnMainThread(() -> { + if (mLoadingDialog != null) { + Log.d(getLogTag(), "showConnectingDialog, is already showing"); + TextView textView = mLoadingDialog.findViewById(R.id.message); + if (textView != null && !message.equals(textView.getText().toString())) { + Log.d(getLogTag(), "showConnectingDialog, update message"); + // TODO: use string res once finalized + textView.setText(message); + } + return; + } + Log.d(getLogTag(), "showConnectingDialog, show dialog"); + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + LayoutInflater inflater = LayoutInflater.from(builder.getContext()); + View customView = inflater.inflate( + R.layout.dialog_audio_sharing_loading_state, /* root= */ + null); + TextView textView = customView.findViewById(R.id.message); + if (textView != null) { + // TODO: use string res once finalized + textView.setText(message); + } + AlertDialog dialog = builder.setView(customView).setCancelable(false).create(); + dialog.setCanceledOnTouchOutside(false); + mLoadingDialog = dialog; + dialog.show(); + }); + } + + private void dismissConnectingDialog() { + postOnMainThread(() -> { + if (mLoadingDialog != null) { + mLoadingDialog.dismiss(); + } + }); + } + + private void postOnMainThread(@NonNull Runnable runnable) { + getContext().getMainExecutor().execute(runnable); + } } diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java index ad41e8a2997..786e1dccc05 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragment.java @@ -16,10 +16,18 @@ package com.android.settings.connecteddevice.audiosharing; -import android.app.settings.SettingsEnums; -import android.content.Context; -import android.os.Bundle; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; + +import android.app.Activity; +import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.settings.R; @@ -27,16 +35,21 @@ import com.android.settings.SettingsActivity; import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.widget.SettingsMainSwitchBar; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.utils.ThreadUtils; public class AudioSharingDashboardFragment extends DashboardFragment implements AudioSharingSwitchBarController.OnAudioSharingStateChangedListener { private static final String TAG = "AudioSharingDashboardFrag"; + public static final int SHARE_THEN_PAIR_REQUEST_CODE = 1002; + SettingsMainSwitchBar mMainSwitchBar; private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController; private AudioSharingCallAudioPreferenceController mAudioSharingCallAudioPreferenceController; private AudioSharingPlaySoundPreferenceController mAudioSharingPlaySoundPreferenceController; private AudioStreamsCategoryController mAudioStreamsCategoryController; + private AudioSharingSwitchBarController mAudioSharingSwitchBarController; public AudioSharingDashboardFragment() { super(); @@ -84,13 +97,38 @@ public class AudioSharingDashboardFragment extends DashboardFragment final SettingsActivity activity = (SettingsActivity) getActivity(); mMainSwitchBar = activity.getSwitchBar(); mMainSwitchBar.setTitle(getText(R.string.audio_sharing_switch_title)); - AudioSharingSwitchBarController switchBarController = + mAudioSharingSwitchBarController = new AudioSharingSwitchBarController(activity, mMainSwitchBar, this); - switchBarController.init(this); - getSettingsLifecycle().addObserver(switchBarController); + mAudioSharingSwitchBarController.init(this); + getSettingsLifecycle().addObserver(mAudioSharingSwitchBarController); mMainSwitchBar.show(); } + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (!BluetoothUtils.isAudioSharingEnabled()) return; + // In share then pair flow, after users be routed to pair new device page and successfully + // pair and connect an LEA headset, the pair fragment will be finished with RESULT_OK + // and EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, pass the BT device to switch bar controller, + // which is responsible for adding source to the device with loading indicator. + if (requestCode == SHARE_THEN_PAIR_REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + BluetoothDevice btDevice = + data != null + ? data.getParcelableExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, + BluetoothDevice.class) + : null; + Log.d(TAG, "onActivityResult: RESULT_OK with device = " + btDevice); + if (btDevice != null) { + var unused = ThreadUtils.postOnBackgroundThread( + () -> mAudioSharingSwitchBarController.handleAutoAddSourceAfterPair( + btDevice)); + } + } + } + } + @Override public void onAudioSharingStateChanged() { updateVisibilityForAttachedPreferences(); @@ -107,11 +145,13 @@ public class AudioSharingDashboardFragment extends DashboardFragment AudioSharingDeviceVolumeGroupController volumeGroupController, AudioSharingCallAudioPreferenceController callAudioController, AudioSharingPlaySoundPreferenceController playSoundController, - AudioStreamsCategoryController streamsCategoryController) { + AudioStreamsCategoryController streamsCategoryController, + AudioSharingSwitchBarController switchBarController) { mAudioSharingDeviceVolumeGroupController = volumeGroupController; mAudioSharingCallAudioPreferenceController = callAudioController; mAudioSharingPlaySoundPreferenceController = playSoundController; mAudioStreamsCategoryController = streamsCategoryController; + mAudioSharingSwitchBarController = switchBarController; } private void updateVisibilityForAttachedPreferences() { diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java index 1ae541ca01d..1b68eaccbfe 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragment.java @@ -16,6 +16,9 @@ package com.android.settings.connecteddevice.audiosharing; +import static com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment.SHARE_THEN_PAIR_REQUEST_CODE; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING; + import android.app.Dialog; import android.app.settings.SettingsEnums; import android.os.Bundle; @@ -48,19 +51,23 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { // The host creates an instance of this dialog fragment must implement this interface to receive // event callbacks. public interface DialogEventListener { + /** Called when users click the positive button in the dialog. */ + default void onPositiveClick() {} + /** * Called when users click the device item for sharing in the dialog. * * @param item The device item clicked. */ - void onItemClick(AudioSharingDeviceItem item); + default void onItemClick(@NonNull AudioSharingDeviceItem item) {} /** Called when users click the cancel button in the dialog. */ - void onCancelClick(); + default void onCancelClick() {} } @Nullable private static DialogEventListener sListener; private static Pair[] sEventData = new Pair[0]; + @Nullable private static Fragment sHost; @Override public int getMetricsCategory() { @@ -70,10 +77,10 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { /** * Display the {@link AudioSharingDialogFragment} dialog. * - * @param host The Fragment this dialog will be hosted. + * @param host The Fragment this dialog will be hosted. * @param deviceItems The connected device items eligible for audio sharing. - * @param listener The callback to handle the user action on this dialog. - * @param eventData The eventData to log with for dialog onClick events. + * @param listener The callback to handle the user action on this dialog. + * @param eventData The eventData to log with for dialog onClick events. */ public static void show( @NonNull Fragment host, @@ -88,6 +95,7 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { Log.d(TAG, "Fail to show dialog: " + e.getMessage()); return; } + sHost = host; sListener = listener; sEventData = eventData; AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG); @@ -136,23 +144,33 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { .setCustomPositiveButton( R.string.audio_sharing_pair_button_label, v -> { - dismiss(); - new SubSettingLauncher(getContext()) - .setDestination(BluetoothPairingDetail.class.getName()) - .setSourceMetricsCategory(getMetricsCategory()) - .launch(); + if (sListener != null) { + sListener.onPositiveClick(); + } logDialogPositiveBtnClick(); + dismiss(); + Bundle args = new Bundle(); + args.putBoolean(EXTRA_PAIR_AND_JOIN_SHARING, true); + SubSettingLauncher launcher = + new SubSettingLauncher(getContext()) + .setDestination( + BluetoothPairingDetail.class.getName()) + .setSourceMetricsCategory(getMetricsCategory()) + .setArguments(args); + if (sHost != null) { + launcher.setResultListener(sHost, SHARE_THEN_PAIR_REQUEST_CODE); + } + launcher.launch(); }) .setCustomNegativeButton( R.string.audio_sharing_qrcode_button_label, v -> { - dismiss(); + onCancelClick(); new SubSettingLauncher(getContext()) .setTitleRes(R.string.audio_streams_qr_code_page_title) .setDestination(AudioStreamsQrCodeFragment.class.getName()) .setSourceMetricsCategory(getMetricsCategory()) .launch(); - logDialogNegativeBtnClick(); }); } else if (deviceItems.size() == 1) { AudioSharingDeviceItem deviceItem = Iterables.getOnlyElement(deviceItems); @@ -166,8 +184,8 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { v -> { if (sListener != null) { sListener.onItemClick(deviceItem); - logDialogPositiveBtnClick(); } + logDialogPositiveBtnClick(); dismiss(); }) .setCustomNegativeButton( @@ -182,8 +200,8 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { (AudioSharingDeviceItem item) -> { if (sListener != null) { sListener.onItemClick(item); - logDialogPositiveBtnClick(); } + logDialogPositiveBtnClick(); dismiss(); }, AudioSharingDeviceAdapter.ActionType.SHARE)) @@ -196,8 +214,8 @@ public class AudioSharingDialogFragment extends InstrumentedDialogFragment { private void onCancelClick() { if (sListener != null) { sListener.onCancelClick(); - logDialogNegativeBtnClick(); } + logDialogNegativeBtnClick(); dismiss(); } diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java index 5de615e968f..aceeb94420e 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragment.java @@ -29,7 +29,6 @@ import androidx.fragment.app.FragmentManager; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settingslib.bluetooth.BluetoothUtils; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFragment { private static final String TAG = "AudioSharingIncompatDlg"; @@ -59,7 +58,7 @@ public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFr * * @param host The Fragment this dialog will be hosted. */ - public static void show(@Nullable Fragment host, @NonNull CachedBluetoothDevice cachedDevice, + public static void show(@Nullable Fragment host, @NonNull String deviceName, @NonNull DialogEventListener listener) { if (host == null || !BluetoothUtils.isAudioSharingEnabled()) return; final FragmentManager manager; @@ -77,7 +76,7 @@ public class AudioSharingIncompatibleDialogFragment extends InstrumentedDialogFr } Log.d(TAG, "Show up the incompatible device dialog."); final Bundle bundle = new Bundle(); - bundle.putString(BUNDLE_KEY_DEVICE_NAME, cachedDevice.getName()); + bundle.putString(BUNDLE_KEY_DEVICE_NAME, deviceName); AudioSharingIncompatibleDialogFragment dialogFrag = new AudioSharingIncompatibleDialogFragment(); dialogFrag.setArguments(bundle); diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java index 79cc56ea6dd..8706590c362 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingLoadingStateDialogFragment.java @@ -115,10 +115,6 @@ public class AudioSharingLoadingStateDialogFragment extends InstrumentedDialogFr @NonNull public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { mHandler = new Handler(Looper.getMainLooper()); - mHandler.postDelayed(() -> { - Log.d(TAG, "Auto dismiss dialog after timeout"); - dismiss(); - }, AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); Bundle args = requireArguments(); String message = args.getString(BUNDLE_KEY_MESSAGE, ""); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); @@ -132,6 +128,26 @@ public class AudioSharingLoadingStateDialogFragment extends InstrumentedDialogFr return dialog; } + @Override + public void onStart() { + super.onStart(); + if (mHandler != null) { + Log.d(TAG, "onStart, postTimeOut for auto dismiss"); + mHandler.postDelayed(() -> { + Log.d(TAG, "Try to auto dismiss dialog after timeout"); + try { + Dialog dialog = getDialog(); + if (dialog != null) { + Log.d(TAG, "Dialog is not null, dismiss"); + dismissAllowingStateLoss(); + } + } catch (IllegalStateException e) { + Log.d(TAG, "Fail to dismiss: " + e.getMessage()); + } + }, AUTO_DISMISS_MESSAGE_ID, AUTO_DISMISS_TIME_THRESHOLD_MS); + } + } + @Override public void onDismiss(@NonNull DialogInterface dialog) { super.onDismiss(dialog); diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java index c0f463d863f..395647ca84a 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarController.java @@ -56,6 +56,7 @@ import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.BluetoothEventManager; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; @@ -78,9 +79,9 @@ import java.util.concurrent.atomic.AtomicInteger; public class AudioSharingSwitchBarController extends BasePreferenceController implements DefaultLifecycleObserver, - OnCheckedChangeListener, - LocalBluetoothProfileManager.ServiceListener, - BluetoothCallback { + OnCheckedChangeListener, + LocalBluetoothProfileManager.ServiceListener, + BluetoothCallback { private static final String TAG = "AudioSharingSwitchCtlr"; private static final String PREF_KEY = "audio_sharing_main_switch"; @@ -464,6 +465,18 @@ public class AudioSharingSwitchBarController extends BasePreferenceController this.mFragment = fragment; } + /** Handle auto add source to the just paired device in share then pair flow. */ + public void handleAutoAddSourceAfterPair(@NonNull BluetoothDevice device) { + CachedBluetoothDeviceManager deviceManager = + mBtManager == null ? null : mBtManager.getCachedDeviceManager(); + CachedBluetoothDevice cachedDevice = + deviceManager == null ? null : deviceManager.findDevice(device); + if (cachedDevice != null) { + Log.d(TAG, "handleAutoAddSourceAfterPair, device = " + device.getAnonymizedAddress()); + addSourceToTargetSinks(ImmutableList.of(device), cachedDevice.getName()); + } + } + /** Test only: set callback registration status in tests. */ @VisibleForTesting void setCallbacksRegistered(boolean registered) { @@ -610,8 +623,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController mMetricsFeatureProvider.action(mContext, SettingsEnums.ACTION_AUTO_JOIN_AUDIO_SHARING); mTargetActiveItem = null; if (mIntentHandleStage.compareAndSet( - StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(), - StartIntentHandleStage.HANDLED.ordinal()) + StartIntentHandleStage.HANDLE_AUTO_ADD.ordinal(), + StartIntentHandleStage.HANDLED.ordinal()) && mDeviceItemsForSharing.size() == 1) { Log.d(TAG, "handleOnBroadcastReady: auto add source to the second device"); AudioSharingDeviceItem target = mDeviceItemsForSharing.get(0); @@ -638,6 +651,13 @@ public class AudioSharingSwitchBarController extends BasePreferenceController private void showDialog(Pair[] eventData) { AudioSharingDialogFragment.DialogEventListener listener = new AudioSharingDialogFragment.DialogEventListener() { + @Override + public void onPositiveClick() { + // Could go to other pages, dismiss the loading dialog. + dismissLoadingStateDialogIfNeeded(); + cleanUp(); + } + @Override public void onItemClick(@NonNull AudioSharingDeviceItem item) { List targetSinks = mGroupedConnectedDevices.getOrDefault( @@ -648,6 +668,7 @@ public class AudioSharingSwitchBarController extends BasePreferenceController @Override public void onCancelClick() { + // Could go to other pages, dismiss the loading dialog. dismissLoadingStateDialogIfNeeded(); cleanUp(); } @@ -669,8 +690,8 @@ public class AudioSharingSwitchBarController extends BasePreferenceController @NonNull ViewGroup host, @NonNull View view, @NonNull AccessibilityEvent event) { if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED && (event.getContentChangeTypes() - & AccessibilityEvent.CONTENT_CHANGE_TYPE_ENABLED) - != 0) { + & AccessibilityEvent.CONTENT_CHANGE_TYPE_ENABLED) + != 0) { Log.d(TAG, "Skip accessibility event for CONTENT_CHANGE_TYPE_ENABLED"); return false; } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java index 40f7895d8dc..e326c1e2630 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBaseTest.java @@ -16,52 +16,80 @@ package com.android.settings.bluetooth; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_PAIR_AND_JOIN_SHARING; + import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; +import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothStatusCodes; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Looper; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.Pair; +import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; import androidx.test.core.app.ApplicationProvider; +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.flags.Flags; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; +import java.util.concurrent.Executor; + /** Tests for {@link BluetoothDevicePairingDetailBase}. */ @RunWith(RobolectricTestRunner.class) @Config(shadows = { ShadowBluetoothAdapter.class, + ShadowAlertDialogCompat.class, com.android.settings.testutils.shadow.ShadowFragment.class, }) public class BluetoothDevicePairingDetailBaseTest { @Rule - public final MockitoRule mockito = MockitoJUnit.rule(); + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); public static final String KEY_DEVICE_LIST_GROUP = "test_key"; @@ -86,8 +114,12 @@ public class BluetoothDevicePairingDetailBaseTest { @Before public void setUp() { mAvailableDevicesCategory = spy(new BluetoothProgressCategory(mContext)); - mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mBluetoothAdapter = spy(BluetoothAdapter.getDefaultAdapter()); mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS); final Pair pairs = new Pair<>(mDrawable, "fake_device"); when(mCachedBluetoothDevice.getDrawableWithDescription()).thenReturn(pairs); @@ -155,8 +187,88 @@ public class BluetoothDevicePairingDetailBaseTest { verify(mFragment).showBluetoothTurnedOnToast(); } + @Test + public void onDeviceBondStateChanged_bonded_pairAndJoinSharingDisabled_finish() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(false); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); + + verify(mFragment).finish(); + } + + @Test + public void onDeviceBondStateChanged_bonded_pairAndJoinSharingEnabled_handle() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); + shadowOf(Looper.getMainLooper()).idle(); + + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNotNull(); + TextView message = dialog.findViewById(R.id.message); + assertThat(message).isNotNull(); + // TODO: use stringr res once finalized + assertThat(message.getText().toString()).isEqualTo( + "Connecting to " + TEST_DEVICE_ADDRESS + "..."); + verify(mFragment, never()).finish(); + } + + @Test + public void onDeviceBondStateChanged_bonding_pairAndJoinSharingDisabled_doNothing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(false); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + + verify(mBluetoothAdapter, never()).addOnMetadataChangedListener(any(BluetoothDevice.class), + any(Executor.class), any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + + @Test + public void onDeviceBondStateChanged_bonding_pairAndJoinSharingEnabled_addListener() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + + verify(mBluetoothAdapter).addOnMetadataChangedListener(eq(mBluetoothDevice), + any(Executor.class), + any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + + @Test + public void onDeviceBondStateChanged_unbonded_pairAndJoinSharingDisabled_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_NONE); + + verify(mBluetoothAdapter, never()).removeOnMetadataChangedListener( + any(BluetoothDevice.class), any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + + @Test + public void onDeviceBondStateChanged_unbonded_pairAndJoinSharingEnabled_removeListener() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDING); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_NONE); + + verify(mBluetoothAdapter).removeOnMetadataChangedListener(eq(mBluetoothDevice), + any(BluetoothAdapter.OnMetadataChangedListener.class)); + } + @Test public void onProfileConnectionStateChanged_deviceInSelectedListAndConnected_finish() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(mBluetoothDevice); mFragment.mSelectedList.add(device); @@ -165,13 +277,43 @@ public class BluetoothDevicePairingDetailBaseTest { when(mCachedBluetoothDevice.getDevice()).thenReturn(device); mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, - BluetoothProfile.A2DP, BluetoothAdapter.STATE_CONNECTED); + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP); verify(mFragment).finish(); } + @Test + public void + onProfileConnectionStateChanged_deviceInSelectedListAndConnected_pairAndJoinSharing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); + mFragment.mSelectedList.add(mBluetoothDevice); + setUpFragmentWithPairAndJoinSharingIntent(true); + mFragment.onDeviceBondStateChanged(mCachedBluetoothDevice, BluetoothDevice.BOND_BONDED); + shadowOf(Looper.getMainLooper()).idle(); + + when(mCachedBluetoothDevice.isConnected()).thenReturn(true); + + mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + shadowOf(Looper.getMainLooper()).idle(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mFragment.getActivity()).setResult(eq(Activity.RESULT_OK), captor.capture()); + Intent intent = captor.getValue(); + BluetoothDevice btDevice = + intent != null + ? intent.getParcelableExtra(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, + BluetoothDevice.class) + : null; + assertThat(btDevice).isNotNull(); + assertThat(btDevice).isEqualTo(mBluetoothDevice); + verify(mFragment).finish(); + } + @Test public void onProfileConnectionStateChanged_deviceNotInSelectedList_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(device); @@ -179,13 +321,14 @@ public class BluetoothDevicePairingDetailBaseTest { when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice); mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, - BluetoothProfile.A2DP, BluetoothAdapter.STATE_CONNECTED); + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP); // not crash } @Test public void onProfileConnectionStateChanged_deviceDisconnected_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(TEST_DEVICE_ADDRESS_B); mFragment.mSelectedList.add(mBluetoothDevice); mFragment.mSelectedList.add(device); @@ -194,13 +337,14 @@ public class BluetoothDevicePairingDetailBaseTest { when(mCachedBluetoothDevice.getDevice()).thenReturn(device); mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, - BluetoothProfile.A2DP, BluetoothAdapter.STATE_DISCONNECTED); + BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.A2DP); // not crash } @Test public void onProfileConnectionStateChanged_deviceInPreferenceMapAndConnected_removed() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final BluetoothDevicePreference preference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, true, BluetoothDevicePreference.SortType.TYPE_FIFO); @@ -211,13 +355,14 @@ public class BluetoothDevicePairingDetailBaseTest { when(mCachedBluetoothDevice.getDevice()).thenReturn(device); mFragment.onProfileConnectionStateChanged(mCachedBluetoothDevice, - BluetoothProfile.A2DP, BluetoothAdapter.STATE_CONNECTED); + BluetoothAdapter.STATE_CONNECTED, BluetoothProfile.A2DP); assertThat(mFragment.getDevicePreferenceMap().size()).isEqualTo(0); } @Test public void onProfileConnectionStateChanged_deviceNotInPreferenceMap_doNothing() { + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); final CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); final BluetoothDevicePreference preference = new BluetoothDevicePreference(mContext, mCachedBluetoothDevice, @@ -233,12 +378,26 @@ public class BluetoothDevicePairingDetailBaseTest { when(cachedDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS_B); when(cachedDevice.getIdentityAddress()).thenReturn(TEST_DEVICE_ADDRESS_B); - mFragment.onProfileConnectionStateChanged(cachedDevice, BluetoothProfile.A2DP, - BluetoothAdapter.STATE_CONNECTED); + mFragment.onProfileConnectionStateChanged(cachedDevice, BluetoothAdapter.STATE_CONNECTED, + BluetoothProfile.A2DP); // not crash } + private void setUpFragmentWithPairAndJoinSharingIntent(boolean enablePairAndJoinSharing) { + Bundle args = new Bundle(); + args.putBoolean(EXTRA_PAIR_AND_JOIN_SHARING, enablePairAndJoinSharing); + Intent intent = new Intent(); + intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); + FragmentActivity activity = spy(Robolectric.setupActivity(FragmentActivity.class)); + doReturn(intent).when(activity).getIntent(); + doReturn(activity).when(mFragment).getActivity(); + FragmentManager fragmentManager = mock(FragmentManager.class); + doReturn(fragmentManager).when(mFragment).getFragmentManager(); + mFragment.mShouldTriggerAudioSharingShareThenPairFlow = + mFragment.shouldTriggerAudioSharingShareThenPairFlow(); + } + private static class TestBluetoothDevicePairingDetailBase extends BluetoothDevicePairingDetailBase { diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java index 7d8846d0506..1ce3316811b 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDashboardFragmentTest.java @@ -16,16 +16,29 @@ package com.android.settings.connecteddevice.audiosharing; +import static com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment.SHARE_THEN_PAIR_REQUEST_CODE; +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE; + import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; +import android.app.Activity; import android.app.settings.SettingsEnums; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothStatusCodes; import android.content.Context; +import android.content.Intent; import android.os.Bundle; +import android.os.Looper; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.View; import androidx.test.core.app.ApplicationProvider; @@ -33,24 +46,29 @@ import androidx.test.core.app.ApplicationProvider; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; import com.android.settings.testutils.shadow.ShadowFragment; import com.android.settings.widget.SettingsMainSwitchBar; +import com.android.settingslib.flags.Flags; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; @RunWith(RobolectricTestRunner.class) -@Config(shadows = {ShadowFragment.class}) +@Config(shadows = {ShadowFragment.class, ShadowBluetoothAdapter.class}) public class AudioSharingDashboardFragmentTest { @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private SettingsActivity mActivity; @Mock private SettingsMainSwitchBar mSwitchBar; @@ -59,11 +77,19 @@ public class AudioSharingDashboardFragmentTest { @Mock private AudioSharingCallAudioPreferenceController mCallAudioController; @Mock private AudioSharingPlaySoundPreferenceController mPlaySoundController; @Mock private AudioStreamsCategoryController mStreamsCategoryController; + @Mock private AudioSharingSwitchBarController mSwitchBarController; private final Context mContext = ApplicationProvider.getApplicationContext(); private AudioSharingDashboardFragment mFragment; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; @Before public void setUp() { + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setEnabled(true); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); when(mSwitchBar.getRootView()).thenReturn(mView); mFragment = new AudioSharingDashboardFragment(); } @@ -100,13 +126,73 @@ public class AudioSharingDashboardFragmentTest { verify(mSwitchBar).show(); } + @Test + public void onActivityResult_shareThenPairWithBadCode_doNothing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mFragment.setControllers( + mVolumeGroupController, + mCallAudioController, + mPlaySoundController, + mStreamsCategoryController, + mSwitchBarController); + Intent data = new Intent(); + Bundle extras = new Bundle(); + BluetoothDevice device = Mockito.mock(BluetoothDevice.class); + extras.putParcelable(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, device); + data.putExtras(extras); + mFragment.onActivityResult(SHARE_THEN_PAIR_REQUEST_CODE, Activity.RESULT_CANCELED, data); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mSwitchBarController, never()).handleAutoAddSourceAfterPair(device); + } + + @Test + public void onActivityResult_shareThenPairWithNoDevice_doNothing() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mFragment.setControllers( + mVolumeGroupController, + mCallAudioController, + mPlaySoundController, + mStreamsCategoryController, + mSwitchBarController); + Intent data = new Intent(); + Bundle extras = new Bundle(); + extras.putParcelable(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, null); + data.putExtras(extras); + mFragment.onActivityResult(SHARE_THEN_PAIR_REQUEST_CODE, Activity.RESULT_CANCELED, data); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mSwitchBarController, never()).handleAutoAddSourceAfterPair(any()); + } + + @Test + public void onActivityResult_shareThenPairWithDevice_handleAutoAddSource() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + mFragment.setControllers( + mVolumeGroupController, + mCallAudioController, + mPlaySoundController, + mStreamsCategoryController, + mSwitchBarController); + Intent data = new Intent(); + Bundle extras = new Bundle(); + BluetoothDevice device = Mockito.mock(BluetoothDevice.class); + extras.putParcelable(EXTRA_BT_DEVICE_TO_AUTO_ADD_SOURCE, device); + data.putExtras(extras); + mFragment.onActivityResult(SHARE_THEN_PAIR_REQUEST_CODE, Activity.RESULT_OK, data); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mSwitchBarController).handleAutoAddSourceAfterPair(device); + } + @Test public void onAudioSharingStateChanged_updateVisibilityForControllers() { mFragment.setControllers( mVolumeGroupController, mCallAudioController, mPlaySoundController, - mStreamsCategoryController); + mStreamsCategoryController, + mSwitchBarController); mFragment.onAudioSharingStateChanged(); verify(mVolumeGroupController).updateVisibility(); verify(mCallAudioController).updateVisibility(); @@ -120,7 +206,8 @@ public class AudioSharingDashboardFragmentTest { mVolumeGroupController, mCallAudioController, mPlaySoundController, - mStreamsCategoryController); + mStreamsCategoryController, + mSwitchBarController); mFragment.onAudioSharingProfilesConnected(); verify(mVolumeGroupController).onAudioSharingProfilesConnected(); } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java index 7227f37998b..dec85e47b80 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingDialogFragmentTest.java @@ -34,6 +34,7 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; @@ -82,11 +83,6 @@ public class AudioSharingDialogFragmentTest { new AudioSharingDeviceItem(TEST_DEVICE_NAME3, /* groupId= */ 3, /* isActive= */ false); private static final AudioSharingDialogFragment.DialogEventListener EMPTY_EVENT_LISTENER = new AudioSharingDialogFragment.DialogEventListener() { - @Override - public void onItemClick(AudioSharingDeviceItem item) {} - - @Override - public void onCancelClick() {} }; private static final Pair TEST_EVENT_DATA = Pair.create(1, 1); private static final Pair[] TEST_EVENT_DATA_LIST = @@ -176,8 +172,17 @@ public class AudioSharingDialogFragmentTest { @Test public void onCreateDialog_noExtraConnectedDevice_pairNewDevice() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + AtomicBoolean isPairBtnClicked = new AtomicBoolean(false); AudioSharingDialogFragment.show( - mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST); + mParent, + new ArrayList<>(), + new AudioSharingDialogFragment.DialogEventListener() { + @Override + public void onPositiveClick() { + isPairBtnClicked.set(true); + } + }, + TEST_EVENT_DATA_LIST); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); assertThat(dialog).isNotNull(); @@ -191,14 +196,24 @@ public class AudioSharingDialogFragmentTest { any(Context.class), eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_POSITIVE_BTN_CLICKED), eq(TEST_EVENT_DATA)); + assertThat(isPairBtnClicked.get()).isTrue(); assertThat(dialog.isShowing()).isFalse(); } @Test public void onCreateDialog_noExtraConnectedDevice_showQRCode() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + AtomicBoolean isQrCodeBtnClicked = new AtomicBoolean(false); AudioSharingDialogFragment.show( - mParent, new ArrayList<>(), EMPTY_EVENT_LISTENER, TEST_EVENT_DATA_LIST); + mParent, + new ArrayList<>(), + new AudioSharingDialogFragment.DialogEventListener() { + @Override + public void onCancelClick() { + isQrCodeBtnClicked.set(true); + } + }, + TEST_EVENT_DATA_LIST); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); assertThat(dialog).isNotNull(); @@ -212,6 +227,7 @@ public class AudioSharingDialogFragmentTest { any(Context.class), eq(SettingsEnums.ACTION_AUDIO_SHARING_DIALOG_NEGATIVE_BTN_CLICKED), eq(TEST_EVENT_DATA)); + assertThat(isQrCodeBtnClicked.get()).isTrue(); assertThat(dialog.isShowing()).isFalse(); } @@ -286,12 +302,9 @@ public class AudioSharingDialogFragmentTest { list, new AudioSharingDialogFragment.DialogEventListener() { @Override - public void onItemClick(AudioSharingDeviceItem item) { + public void onItemClick(@NonNull AudioSharingDeviceItem item) { isShareBtnClicked.set(true); } - - @Override - public void onCancelClick() {} }, TEST_EVENT_DATA_LIST); shadowMainLooper().idle(); @@ -359,9 +372,6 @@ public class AudioSharingDialogFragmentTest { mParent, list, new AudioSharingDialogFragment.DialogEventListener() { - @Override - public void onItemClick(AudioSharingDeviceItem item) {} - @Override public void onCancelClick() { isCancelBtnClicked.set(true); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java index 7f172918d36..67cb2aa915b 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingIncompatibleDialogFragmentTest.java @@ -18,7 +18,6 @@ package com.android.settings.connecteddevice.audiosharing; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.when; import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; import android.bluetooth.BluetoothAdapter; @@ -34,7 +33,6 @@ import androidx.fragment.app.FragmentActivity; import com.android.settings.R; import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.flags.Flags; import org.junit.After; @@ -42,7 +40,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; @@ -62,7 +59,6 @@ public class AudioSharingIncompatibleDialogFragmentTest { @Rule public final MockitoRule mocks = MockitoJUnit.rule(); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Mock private CachedBluetoothDevice mCachedBluetoothDevice; private Fragment mParent; private AudioSharingIncompatibleDialogFragment mFragment; @@ -76,7 +72,6 @@ public class AudioSharingIncompatibleDialogFragmentTest { BluetoothStatusCodes.FEATURE_SUPPORTED); shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( BluetoothStatusCodes.FEATURE_SUPPORTED); - when(mCachedBluetoothDevice.getName()).thenReturn(TEST_DEVICE_NAME); mFragment = new AudioSharingIncompatibleDialogFragment(); mParent = new Fragment(); FragmentController.setupFragment(mParent, FragmentActivity.class, /* containerViewId= */ @@ -97,7 +92,7 @@ public class AudioSharingIncompatibleDialogFragmentTest { @Test public void onCreateDialog_flagOff_dialogNotExist() { mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - AudioSharingIncompatibleDialogFragment.show(mParent, mCachedBluetoothDevice, + AudioSharingIncompatibleDialogFragment.show(mParent, TEST_DEVICE_NAME, EMPTY_EVENT_LISTENER); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); @@ -107,7 +102,7 @@ public class AudioSharingIncompatibleDialogFragmentTest { @Test public void onCreateDialog_unattachedFragment_dialogNotExist() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - AudioSharingIncompatibleDialogFragment.show(new Fragment(), mCachedBluetoothDevice, + AudioSharingIncompatibleDialogFragment.show(new Fragment(), TEST_DEVICE_NAME, EMPTY_EVENT_LISTENER); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); @@ -117,7 +112,7 @@ public class AudioSharingIncompatibleDialogFragmentTest { @Test public void onCreateDialog_flagOn_showDialog() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); - AudioSharingIncompatibleDialogFragment.show(mParent, mCachedBluetoothDevice, + AudioSharingIncompatibleDialogFragment.show(mParent, TEST_DEVICE_NAME, EMPTY_EVENT_LISTENER); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); @@ -134,7 +129,7 @@ public class AudioSharingIncompatibleDialogFragmentTest { public void onCreateDialog_clickBtn_callbackTriggered() { mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); AtomicBoolean isBtnClicked = new AtomicBoolean(false); - AudioSharingIncompatibleDialogFragment.show(mParent, mCachedBluetoothDevice, + AudioSharingIncompatibleDialogFragment.show(mParent, TEST_DEVICE_NAME, () -> isBtnClicked.set(true)); shadowMainLooper().idle(); AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java index 0d21f18b821..eb2083ebe7b 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingSwitchBarControllerTest.java @@ -934,6 +934,19 @@ public class AudioSharingSwitchBarControllerTest { childFragments.forEach(fragment -> ((DialogFragment) fragment).dismiss()); } + @Test + public void handleAutoAddSourceAfterPair() { + when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(mDevice1)); + when(mBroadcast.getLatestBluetoothLeBroadcastMetadata()).thenReturn(mMetadata); + mController.handleAutoAddSourceAfterPair(mDevice1); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mAssistant).addSource(mDevice1, mMetadata, /* isGroupOp= */ false); + List childFragments = mParentFragment.getChildFragmentManager().getFragments(); + assertThat(childFragments).comparingElementsUsing(CLAZZNAME_EQUALS).containsExactly( + AudioSharingLoadingStateDialogFragment.class.getName()); + } + private Fragment setUpFragmentWithStartSharingIntent() { Bundle args = new Bundle(); args.putBoolean(EXTRA_START_LE_AUDIO_SHARING, true); From 5f94ee807c002f812cfa3e543f8ab4624e7a845b Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Thu, 19 Sep 2024 15:53:14 +0800 Subject: [PATCH 09/12] Disable toggles if isAllowedChangingState is false BUG: 343317785 Test: local test Flag: com.android.settings.flags.enable_bluetooth_device_details_polish Change-Id: Iaf53773bd47c35b742dc81a30b5d2fe459e65878 --- .../ui/composable/MultiTogglePreferenceGroup.kt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt index d29795efee7..9743737f515 100644 --- a/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt +++ b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -62,6 +63,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.DialogProperties @@ -78,7 +80,11 @@ fun MultiTogglePreferenceGroup( var settingIdForPopUp by remember { mutableStateOf(null) } settingIdForPopUp?.let { id -> - preferenceModels.find { it.id == id }?.let { dialog(it) { settingIdForPopUp = null } } + preferenceModels.find { it.id == id && it.isAllowedChangingState }?.let { + dialog(it) { settingIdForPopUp = null } + } ?: run { + settingIdForPopUp = null + } } Row( @@ -102,7 +108,9 @@ fun MultiTogglePreferenceGroup( Modifier.fillMaxSize().padding(8.dp).semantics { role = Role.Switch toggleableState = - if (preferenceModel.isActive) { + if (!preferenceModel.isAllowedChangingState) { + ToggleableState.Indeterminate + } else if (preferenceModel.isActive) { ToggleableState.On } else { ToggleableState.Off @@ -110,6 +118,7 @@ fun MultiTogglePreferenceGroup( contentDescription = preferenceModel.title }, onClick = { settingIdForPopUp = preferenceModel.id }, + enabled = preferenceModel.isAllowedChangingState, shape = RoundedCornerShape(20.dp), colors = getButtonColors(preferenceModel.isActive), contentPadding = PaddingValues(0.dp)) { @@ -254,7 +263,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingPreferenceModel.Mu } Spacer(modifier = Modifier.height(12.dp)) Row( - modifier = Modifier.fillMaxWidth().height(32.dp), + modifier = Modifier.fillMaxWidth().defaultMinSize(32.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceEvenly, ) { @@ -263,6 +272,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingPreferenceModel.Mu text = toggle.label, fontSize = 12.sp, textAlign = TextAlign.Center, + overflow = TextOverflow.Visible, modifier = Modifier.weight(1f).padding(horizontal = 8.dp)) } } From 56ff329900d81066ae7efc5f03e47b91a88b781f Mon Sep 17 00:00:00 2001 From: tomhsu Date: Thu, 19 Sep 2024 02:52:14 +0000 Subject: [PATCH 10/12] Fix crash when phone process has problem. - Phone process may not exist in some situation. It shall catch the exception to avoid Settings crash. Flag: EXEMPT bugfix fix: 365669913 Test: build pass Change-Id: I2b95f46b8887a9c8416fb4a5724537ad27e86850 --- .../network/telephony/VoNrRepository.kt | 25 ++++++++++++++---- .../network/telephony/VoNrRepositoryTest.kt | 26 ++++++++++++++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/com/android/settings/network/telephony/VoNrRepository.kt b/src/com/android/settings/network/telephony/VoNrRepository.kt index 635c57223be..7f3823b6481 100644 --- a/src/com/android/settings/network/telephony/VoNrRepository.kt +++ b/src/com/android/settings/network/telephony/VoNrRepository.kt @@ -19,6 +19,7 @@ package com.android.settings.network.telephony import android.content.Context import android.telephony.CarrierConfigManager import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -37,12 +38,13 @@ class VoNrRepository( fun isVoNrAvailable(subId: Int): Boolean { if (!nrRepository.isNrAvailable(subId)) return false data class Config(val isVoNrEnabled: Boolean, val isVoNrSettingVisibility: Boolean) + val carrierConfig = carrierConfigRepository.transformConfig(subId) { Config( isVoNrEnabled = getBoolean(CarrierConfigManager.KEY_VONR_ENABLED_BOOL), isVoNrSettingVisibility = - getBoolean(CarrierConfigManager.KEY_VONR_SETTING_VISIBILITY_BOOL), + getBoolean(CarrierConfigManager.KEY_VONR_SETTING_VISIBILITY_BOOL), ) } return carrierConfig.isVoNrEnabled && carrierConfig.isVoNrSettingVisibility @@ -52,7 +54,14 @@ class VoNrRepository( val telephonyManager = context.telephonyManager(subId) return context .subscriptionsChangedFlow() - .map { telephonyManager.isVoNrEnabled } + .map { + try { + telephonyManager.isVoNrEnabled + } catch (e: IllegalStateException) { + Log.e(TAG, "IllegalStateException - isVoNrEnabled : $e") + false + } + } .conflate() .onEach { Log.d(TAG, "[$subId] isVoNrEnabled: $it") } .flowOn(Dispatchers.Default) @@ -61,11 +70,17 @@ class VoNrRepository( suspend fun setVoNrEnabled(subId: Int, enabled: Boolean) = withContext(Dispatchers.Default) { if (!SubscriptionManager.isValidSubscriptionId(subId)) return@withContext - val result = context.telephonyManager(subId).setVoNrEnabled(enabled) - Log.d(TAG, "[$subId] setVoNrEnabled: $enabled, result: $result") + var result = TelephonyManager.ENABLE_VONR_RADIO_INVALID_STATE + try { + result = context.telephonyManager(subId).setVoNrEnabled(enabled) + } catch (e: IllegalStateException) { + Log.e(TAG, "IllegalStateException - setVoNrEnabled : $e") + } finally { + Log.d(TAG, "[$subId] setVoNrEnabled: $enabled, result: $result") + } } private companion object { private const val TAG = "VoNrRepository" } -} +} \ No newline at end of file diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/VoNrRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/VoNrRepositoryTest.kt index 90d0aa56d77..265cd33f3bf 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/VoNrRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/VoNrRepositoryTest.kt @@ -27,7 +27,9 @@ import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock import org.mockito.kotlin.spy import org.mockito.kotlin.stub @@ -134,6 +136,15 @@ class VoNrRepositoryTest { assertThat(isVoNrEnabled).isTrue() } + @Test + fun isVoNrEnabledFlow_noPhoneProcess_noCrash() = runBlocking { + mockTelephonyManager.stub { on { isVoNrEnabled } doThrow IllegalStateException("no Phone") } + + val isVoNrEnabled = repository.isVoNrEnabledFlow(SUB_ID).firstWithTimeoutOrNull() + + assertThat(isVoNrEnabled).isFalse() + } + @Test fun setVoNrEnabled(): Unit = runBlocking { repository.setVoNrEnabled(SUB_ID, true) @@ -141,7 +152,20 @@ class VoNrRepositoryTest { verify(mockTelephonyManager).setVoNrEnabled(true) } + @Test + fun setVoNrEnabled_noPhoneProcess_noCrash(): Unit = runBlocking { + mockTelephonyManager.stub { + on { + setVoNrEnabled(any()) + } doThrow IllegalStateException("no Phone") + } + + repository.setVoNrEnabled(SUB_ID, true) + + verify(mockTelephonyManager).setVoNrEnabled(true) + } + private companion object { const val SUB_ID = 1 } -} +} \ No newline at end of file From c6739e4914ed4f2f3d8fcb7980c1d4ca8ee37806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Thu, 19 Sep 2024 14:23:10 +0200 Subject: [PATCH 11/12] Shorten the dark theme effect description in Modes Settings Fixes: 367214085 Test: N/A Flag: android.app.modes_ui Change-Id: I598aab9a98e44cb78a8e486df135a55da4d8e8fc --- res/values/strings.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 82c1e61dea8..06b48a67ed5 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -8246,8 +8246,7 @@ enable dark theme - Switch the OS and apps to prefer light text on a dark - background, which may be easier on the eyes and confers significant battery savings on some devices + Switch the device theme to use a dark background, which is easier on the eyes {count, plural, offset:2 From 5406db9d6eff187a81a2061220a7cc34e399483a Mon Sep 17 00:00:00 2001 From: George Lin Date: Wed, 18 Sep 2024 14:23:32 +0000 Subject: [PATCH 12/12] Adding launch source info in the wallpaper picker intent (1/3) When launching Wallpaper & Style from the settings search, we should also append the launch source information to the intent. The major purpose if for logging. So that we know then entrypoint of the Wallpaper & Style app. Test: Manually tested that the intent contains the extra we need Bug: 368052505 Flag: EXEMPT bugfix Change-Id: I9af663dad7cb79bfe74431e6a61cd34393e60dbd --- .../wallpaper/WallpaperSuggestionActivity.java | 13 +++++++------ .../wallpaper/WallpaperSuggestionActivityTest.java | 4 +++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java b/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java index 14ef4833588..00bd0f23cdc 100644 --- a/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java +++ b/src/com/android/settings/wallpaper/WallpaperSuggestionActivity.java @@ -41,19 +41,20 @@ public class WallpaperSuggestionActivity extends StyleSuggestionActivityBase imp private static final String WALLPAPER_FOCUS = "focus_wallpaper"; private static final String WALLPAPER_ONLY = "wallpaper_only"; private static final String LAUNCHED_SUW = "app_launched_suw"; - - private String mWallpaperLaunchExtra; + private static final String LAUNCH_SOURCE_SETTINGS_SEARCH = "app_launched_settings_search"; @Override protected void addExtras(Intent intent) { + String wallpaperLaunchExtra = + getResources().getString(R.string.config_wallpaper_picker_launch_extra);; if (WizardManagerHelper.isAnySetupWizard(intent)) { intent.putExtra(WALLPAPER_FLAVOR_EXTRA, WALLPAPER_ONLY); - - mWallpaperLaunchExtra = - getResources().getString(R.string.config_wallpaper_picker_launch_extra); - intent.putExtra(mWallpaperLaunchExtra, LAUNCHED_SUW); + intent.putExtra(wallpaperLaunchExtra, LAUNCHED_SUW); } else { + // This is the case when user enter the wallpaper picker from the search result entry + // on the Settings app intent.putExtra(WALLPAPER_FLAVOR_EXTRA, WALLPAPER_FOCUS); + intent.putExtra(wallpaperLaunchExtra, LAUNCH_SOURCE_SETTINGS_SEARCH); } } diff --git a/tests/robotests/src/com/android/settings/wallpaper/WallpaperSuggestionActivityTest.java b/tests/robotests/src/com/android/settings/wallpaper/WallpaperSuggestionActivityTest.java index 3f6d785463d..230b443f296 100644 --- a/tests/robotests/src/com/android/settings/wallpaper/WallpaperSuggestionActivityTest.java +++ b/tests/robotests/src/com/android/settings/wallpaper/WallpaperSuggestionActivityTest.java @@ -118,7 +118,7 @@ public class WallpaperSuggestionActivityTest { } @Test - public void addExtras_intentNotFromSetupWizard_extrasHasFocusWallpaper() { + public void addExtras_intentNotFromSetupWizard_extrasHasFocusWallpaperAndLaunchedSettingsSearch() { WallpaperSuggestionActivity activity = Robolectric.buildActivity( WallpaperSuggestionActivity.class, new Intent(Intent.ACTION_MAIN).setComponent( new ComponentName(RuntimeEnvironment.application, @@ -127,6 +127,8 @@ public class WallpaperSuggestionActivityTest { assertThat(intent).isNotNull(); assertThat(intent.getStringExtra(WALLPAPER_FLAVOR)).isEqualTo("focus_wallpaper"); + assertThat(intent.getStringExtra(WALLPAPER_LAUNCH_SOURCE)) + .isEqualTo("app_launched_settings_search"); }