diff --git a/res/drawable/volume_dialog_button_background_outline.xml b/res/drawable/volume_dialog_button_background_outline.xml index 78f3fcf3c5b..efbaec63bd5 100644 --- a/res/drawable/volume_dialog_button_background_outline.xml +++ b/res/drawable/volume_dialog_button_background_outline.xml @@ -14,18 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - - - - \ No newline at end of file + android:insetTop="6dp" + android:insetBottom="6dp"> + + + + + + + \ No newline at end of file diff --git a/res/drawable/volume_dialog_button_background_solid.xml b/res/drawable/volume_dialog_button_background_solid.xml index 1fa8f20cf31..697131db247 100644 --- a/res/drawable/volume_dialog_button_background_solid.xml +++ b/res/drawable/volume_dialog_button_background_solid.xml @@ -15,17 +15,20 @@ limitations under the License. --> - - - - - - \ No newline at end of file + android:insetTop="6dp" + android:insetBottom="6dp"> + + + + + + + \ No newline at end of file diff --git a/res/layout/panel_layout.xml b/res/layout/panel_layout.xml index 2b9daebf85d..f154abc40cb 100644 --- a/res/layout/panel_layout.xml +++ b/res/layout/panel_layout.xml @@ -117,7 +117,7 @@ android:id="@+id/see_more" style="@style/PanelOptionRoundedOutlinedButton" android:layout_width="wrap_content" - android:layout_height="36dp" + android:layout_height="wrap_content" android:minWidth="0dp" android:text="@string/settings_button"/> @@ -130,7 +130,7 @@ android:id="@+id/done" style="@style/PanelOptionRoundedSolidButton" android:layout_width="wrap_content" - android:layout_height="36dp" + android:layout_height="wrap_content" android:minWidth="0dp" android:text="@string/done"/> diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index b2de0041b1b..8ee4ebac502 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -117,6 +117,7 @@ import com.android.settingslib.widget.AdaptiveIcon; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Set; public final class Utils extends com.android.settingslib.Utils { @@ -589,7 +590,9 @@ public final class Utils extends com.android.settingslib.Utils { return inflater.inflate(resId, parent, false); } - public static ArraySet getHandledDomains(PackageManager pm, String packageName) { + /** Gets all the domains that the given package could handled. */ + @NonNull + public static Set getHandledDomains(PackageManager pm, String packageName) { final List iviList = pm.getIntentFilterVerifications(packageName); final List filters = pm.getAllIntentFilters(packageName); @@ -597,9 +600,7 @@ public final class Utils extends com.android.settingslib.Utils { final ArraySet result = new ArraySet<>(); if (iviList != null && iviList.size() > 0) { for (IntentFilterVerificationInfo ivi : iviList) { - for (String host : ivi.getDomains()) { - result.add(host); - } + result.addAll(ivi.getDomains()); } } if (filters != null && filters.size() > 0) { diff --git a/src/com/android/settings/applications/OpenSupportedLinks.java b/src/com/android/settings/applications/OpenSupportedLinks.java index 4f5f2a8001c..c4e478cefcb 100644 --- a/src/com/android/settings/applications/OpenSupportedLinks.java +++ b/src/com/android/settings/applications/OpenSupportedLinks.java @@ -23,7 +23,6 @@ import android.app.settings.SettingsEnums; import android.content.pm.PackageManager; import android.os.Bundle; import android.text.TextUtils; -import android.util.ArraySet; import android.util.Log; import android.view.View; @@ -36,6 +35,8 @@ import com.android.settings.Utils; import com.android.settingslib.widget.FooterPreference; import com.android.settingslib.widget.SelectorWithWidgetPreference; +import java.util.Set; + /** * Display the Open Supported Links page. Allow users choose what kind supported links they need. */ @@ -195,7 +196,7 @@ public class OpenSupportedLinks extends AppInfoWithHeader implements @VisibleForTesting void addLinksToFooter(FooterPreference footer) { - final ArraySet result = Utils.getHandledDomains(mPackageManager, mPackageName); + final Set result = Utils.getHandledDomains(mPackageManager, mPackageName); if (result.isEmpty()) { Log.w(TAG, "Can't find any app links."); return; diff --git a/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java b/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java index 49f676ed508..19f3a46aaee 100644 --- a/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java @@ -22,6 +22,7 @@ import android.os.AsyncTask; import android.os.BatteryUsageStats; import android.os.Bundle; import android.os.UidBatteryConsumer; +import android.os.UserHandle; import android.os.UserManager; import android.util.Log; @@ -144,7 +145,7 @@ public class AppBatteryPreferenceController extends BasePreferenceController } else { Log.i(TAG, "Launch : " + mPackageName + " with package name"); AdvancedPowerUsageDetail.startBatteryDetailPage(mParent.getActivity(), mParent, - mPackageName); + mPackageName, UserHandle.CURRENT); } return true; } diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java index 80b49312475..b678e24b5e2 100644 --- a/src/com/android/settings/applications/manageapplications/ManageApplications.java +++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java @@ -686,7 +686,8 @@ public class ManageApplications extends InstrumentedFragment break; case LIST_TYPE_BATTERY_OPTIMIZATION: AdvancedPowerUsageDetail.startBatteryDetailPage( - getActivity(), this, mCurrentPkgName); + getActivity(), this, mCurrentPkgName, + UserHandle.getUserHandleForUid(mCurrentUid)); break; // TODO: Figure out if there is a way where we can spin up the profile's settings // process ahead of time, to avoid a long load of data when user clicks on a managed diff --git a/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java b/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java index a62bbe10e0c..8542fcd2461 100644 --- a/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java +++ b/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java @@ -24,7 +24,6 @@ import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager.NameNotFoundException; import android.os.PowerManager; import android.os.UserManager; import android.util.Log; @@ -126,15 +125,8 @@ public final class BluetoothPermissionRequest extends BroadcastReceiver { // Create an intent triggered by clicking on the // "Clear All Notifications" button - String bluetoothName; - try { - bluetoothName = Utils.findBluetoothPackageName(context); - } catch (NameNotFoundException e) { - e.printStackTrace(); - return; - } Intent deleteIntent = new Intent(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY); - deleteIntent.setPackage(bluetoothName); + deleteIntent.setPackage("com.android.bluetooth"); deleteIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); deleteIntent.putExtra(BluetoothDevice.EXTRA_CONNECTION_ACCESS_RESULT, BluetoothDevice.CONNECTION_ACCESS_NO); diff --git a/src/com/android/settings/bluetooth/Utils.java b/src/com/android/settings/bluetooth/Utils.java old mode 100644 new mode 100755 index 24fe4e151ff..ca8f9d39a8c --- a/src/com/android/settings/bluetooth/Utils.java +++ b/src/com/android/settings/bluetooth/Utils.java @@ -16,18 +16,11 @@ package com.android.settings.bluetooth; -import static android.os.Process.BLUETOOTH_UID; - import android.app.settings.SettingsEnums; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.content.DialogInterface; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.Settings; import android.text.TextUtils; @@ -196,48 +189,4 @@ public final class Utils { } return false; } - - /** - * Returns the Bluetooth Package name - */ - public static String findBluetoothPackageName(Context context) - throws NameNotFoundException { - // this activity will always be in the package where the rest of Bluetooth lives - String sentinelActivity = "com.android.bluetooth.opp.BluetoothOppLauncherActivity"; - PackageManager packageManager = context.createContextAsUser(UserHandle.SYSTEM, 0) - .getPackageManager(); - String[] allPackages = packageManager.getPackagesForUid(BLUETOOTH_UID); - String matchedPackage = null; - for (String candidatePackage : allPackages) { - PackageInfo packageInfo; - try { - packageInfo = - packageManager.getPackageInfo( - candidatePackage, - PackageManager.GET_ACTIVITIES - | PackageManager.MATCH_ANY_USER - | PackageManager.MATCH_UNINSTALLED_PACKAGES - | PackageManager.MATCH_DISABLED_COMPONENTS); - } catch (NameNotFoundException e) { - // rethrow - throw e; - } - if (packageInfo.activities == null) { - continue; - } - for (ActivityInfo activity : packageInfo.activities) { - if (sentinelActivity.equals(activity.name)) { - if (matchedPackage == null) { - matchedPackage = candidatePackage; - } else { - throw new NameNotFoundException("multiple main bluetooth packages found"); - } - } - } - } - if (matchedPackage != null) { - return matchedPackage; - } - throw new NameNotFoundException("Could not find main bluetooth package"); - } } diff --git a/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceController.java b/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceController.java index f1677f29509..bd8169a80fe 100644 --- a/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceController.java +++ b/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceController.java @@ -16,8 +16,9 @@ package com.android.settings.development; -import android.bluetooth.BluetoothManager; import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; import android.os.SystemProperties; import androidx.annotation.VisibleForTesting; @@ -41,15 +42,18 @@ public class BluetoothMaxConnectedAudioDevicesPreferenceController extends private int mDefaultMaxConnectedAudioDevices = 0; - private final BluetoothManager mBluetoothManager; - public BluetoothMaxConnectedAudioDevicesPreferenceController(Context context) { super(context); - mBluetoothManager = context.getSystemService(BluetoothManager.class); - - mDefaultMaxConnectedAudioDevices = - mBluetoothManager.getAdapter().getMaxConnectedAudioDevices(); + try { + Resources res = context.getPackageManager().getResourcesForApplication( + "com.android.bluetooth"); + mDefaultMaxConnectedAudioDevices = res.getInteger(res.getIdentifier( + "config_bluetooth_max_connected_audio_devices", + "integer", "com.android.bluetooth")); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } } @Override diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java index c08a1b8985e..9705154d7b3 100644 --- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java +++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java @@ -219,7 +219,8 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements * Start packageName's battery detail page. */ public static void startBatteryDetailPage( - Activity caller, Instrumentable instrumentable, String packageName) { + Activity caller, Instrumentable instrumentable, String packageName, + UserHandle userHandle) { final Bundle args = new Bundle(3); final PackageManager packageManager = caller.getPackageManager(); args.putString(EXTRA_PACKAGE_NAME, packageName); @@ -235,6 +236,7 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements .setTitleRes(R.string.battery_details_title) .setArguments(args) .setSourceMetricsCategory(instrumentable.getMetricsCategory()) + .setUserHandle(userHandle) .launch(); } @@ -475,16 +477,12 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements private CharSequence getAppActiveTime(Bundle bundle) { final long foregroundTimeMs = bundle.getLong(EXTRA_FOREGROUND_TIME); final long backgroundTimeMs = bundle.getLong(EXTRA_BACKGROUND_TIME); - final int consumedPower = bundle.getInt(EXTRA_POWER_USAGE_AMOUNT); - final int uid = bundle.getInt(EXTRA_UID, 0); final String slotTime = bundle.getString(EXTRA_SLOT_TIME, null); final long totalTimeMs = foregroundTimeMs + backgroundTimeMs; final CharSequence usageTimeSummary; if (totalTimeMs == 0) { - usageTimeSummary = getText( - consumedPower > 0 ? R.string.battery_usage_without_time - : R.string.battery_not_usage); + usageTimeSummary = getText(R.string.battery_usage_without_time); } else if (slotTime == null) { // Shows summary text with last full charge if slot time is null. usageTimeSummary = getAppFullChargeActiveSummary( diff --git a/src/com/android/settings/notification/NotificationVolumePreferenceController.java b/src/com/android/settings/notification/NotificationVolumePreferenceController.java index 322bb6c229b..112debca647 100644 --- a/src/com/android/settings/notification/NotificationVolumePreferenceController.java +++ b/src/com/android/settings/notification/NotificationVolumePreferenceController.java @@ -16,6 +16,7 @@ package com.android.settings.notification; +import android.app.ActivityThread; import android.app.INotificationManager; import android.app.NotificationManager; import android.content.BroadcastReceiver; @@ -29,26 +30,32 @@ import android.os.Looper; import android.os.Message; import android.os.ServiceManager; import android.os.Vibrator; +import android.provider.DeviceConfig; import android.service.notification.NotificationListenerService; import android.text.TextUtils; import android.util.Log; import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.PreferenceScreen; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.settings.R; import com.android.settings.Utils; import com.android.settingslib.core.lifecycle.Lifecycle; import java.util.Objects; +import java.util.Set; /** - * Update notification volume icon in Settings in response to user adjusting volume + * Update notification volume icon in Settings in response to user adjusting volume. */ public class NotificationVolumePreferenceController extends VolumeSeekBarPreferenceController { private static final String TAG = "NotificationVolumePreferenceController"; private static final String KEY_NOTIFICATION_VOLUME = "notification_volume"; + private static final boolean CONFIG_DEFAULT_VAL = false; + private boolean mSeparateNotification; private Vibrator mVibrator; private int mRingerMode = AudioManager.RINGER_MODE_NORMAL; @@ -56,39 +63,74 @@ public class NotificationVolumePreferenceController extends VolumeSeekBarPrefere private final RingReceiver mReceiver = new RingReceiver(); private final H mHandler = new H(); private INotificationManager mNoMan; - - private int mMuteIcon; private final int mNormalIconId = R.drawable.ic_notifications; private final int mVibrateIconId = R.drawable.ic_volume_ringer_vibrate; private final int mSilentIconId = R.drawable.ic_notifications_off_24dp; - private final boolean mRingNotificationAliased; - - public NotificationVolumePreferenceController(Context context) { this(context, KEY_NOTIFICATION_VOLUME); } public NotificationVolumePreferenceController(Context context, String key) { super(context, key); + mVibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); if (mVibrator != null && !mVibrator.hasVibrator()) { mVibrator = null; } - mRingNotificationAliased = mContext.getResources().getBoolean( - com.android.internal.R.bool.config_alias_ring_notif_stream_types); updateRingerMode(); } + /** + * Allow for notification slider to be enabled in the scenario where the config switches on + * while settings page is already on the screen by always configuring the preference, even if it + * is currently inactive. + */ + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + if (mPreference == null) { + setupVolPreference(screen); + } + mSeparateNotification = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, CONFIG_DEFAULT_VAL); + if (mPreference != null) { + mPreference.setVisible(getAvailabilityStatus() == AVAILABLE); + } + updateEffectsSuppressor(); + updatePreferenceIconAndSliderState(); + } + + /** + * Only display the notification slider when the corresponding device config flag is set + */ + private void onDeviceConfigChange(DeviceConfig.Properties properties) { + Set changeSet = properties.getKeyset(); + + if (changeSet.contains(SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION)) { + boolean newVal = properties.getBoolean( + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, CONFIG_DEFAULT_VAL); + if (newVal != mSeparateNotification) { + mSeparateNotification = newVal; + // manually hiding the preference because being unavailable does not do the job + if (mPreference != null) { + mPreference.setVisible(getAvailabilityStatus() == AVAILABLE); + } + } + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @Override public void onResume() { super.onResume(); mReceiver.register(true); - updateEffectsSuppressor(); - updatePreferenceIconAndSliderState(); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + ActivityThread.currentApplication().getMainExecutor(), + this::onDeviceConfigChange); } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) @@ -96,16 +138,17 @@ public class NotificationVolumePreferenceController extends VolumeSeekBarPrefere public void onPause() { super.onPause(); mReceiver.register(false); + DeviceConfig.removeOnPropertiesChangedListener(this::onDeviceConfigChange); } @Override public int getAvailabilityStatus() { + boolean separateNotification = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false); - // Show separate notification slider if ring/notification are not aliased by AudioManager -- - // if they are, notification volume is controlled by RingVolumePreferenceController. return mContext.getResources().getBoolean(R.bool.config_show_notification_volume) - && (!mRingNotificationAliased || !Utils.isVoiceCapable(mContext)) && !mHelper.isSingleVolume() + && (separateNotification || !Utils.isVoiceCapable(mContext)) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } diff --git a/src/com/android/settings/notification/RingVolumePreferenceController.java b/src/com/android/settings/notification/RingVolumePreferenceController.java index a78689f5a0a..7fdb1e16141 100644 --- a/src/com/android/settings/notification/RingVolumePreferenceController.java +++ b/src/com/android/settings/notification/RingVolumePreferenceController.java @@ -16,6 +16,7 @@ package com.android.settings.notification; +import android.app.ActivityThread; import android.app.INotificationManager; import android.app.NotificationManager; import android.content.BroadcastReceiver; @@ -29,6 +30,7 @@ import android.os.Looper; import android.os.Message; import android.os.ServiceManager; import android.os.Vibrator; +import android.provider.DeviceConfig; import android.service.notification.NotificationListenerService; import android.text.TextUtils; import android.util.Log; @@ -36,11 +38,13 @@ import android.util.Log; import androidx.lifecycle.OnLifecycleEvent; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.settings.R; import com.android.settings.Utils; import com.android.settingslib.core.lifecycle.Lifecycle; import java.util.Objects; +import java.util.Set; /** * This slider can represent both ring and notification, if the corresponding streams are aliased, @@ -59,24 +63,21 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr private int mMuteIcon; - /* - * Whether ring and notification streams are aliased together by AudioManager. - * If they are, we'll present one volume control for both. - * If not, we'll present separate volume controls. - */ - private final boolean mRingAliasNotif; - - private final int mNormalIconId; + private int mNormalIconId; @VisibleForTesting - final int mVibrateIconId; + int mVibrateIconId; @VisibleForTesting - final int mSilentIconId; + int mSilentIconId; @VisibleForTesting - final int mTitleId; + int mTitleId; + + private boolean mSeparateNotification; private INotificationManager mNoMan; + private static final boolean CONFIG_DEFAULT_VAL = false; + public RingVolumePreferenceController(Context context) { this(context, KEY_RING_VOLUME); } @@ -87,29 +88,56 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr if (mVibrator != null && !mVibrator.hasVibrator()) { mVibrator = null; } - - mRingAliasNotif = isRingAliasNotification(); - if (mRingAliasNotif) { - mTitleId = R.string.ring_volume_option_title; - - mNormalIconId = R.drawable.ic_notifications; - mSilentIconId = R.drawable.ic_notifications_off_24dp; - } else { - mTitleId = R.string.separate_ring_volume_option_title; - - mNormalIconId = R.drawable.ic_ring_volume; - mSilentIconId = R.drawable.ic_ring_volume_off; - } - // todo: set a distinct vibrate icon for ring vs notification - mVibrateIconId = R.drawable.ic_volume_ringer_vibrate; - + mSeparateNotification = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, CONFIG_DEFAULT_VAL); + loadPreferenceIconResources(mSeparateNotification); updateRingerMode(); } - @VisibleForTesting - boolean isRingAliasNotification() { - return mContext.getResources().getBoolean( - com.android.internal.R.bool.config_alias_ring_notif_stream_types); + private void loadPreferenceIconResources(boolean separateNotification) { + if (separateNotification) { + mTitleId = R.string.separate_ring_volume_option_title; + mNormalIconId = R.drawable.ic_ring_volume; + mSilentIconId = R.drawable.ic_ring_volume_off; + } else { + mTitleId = R.string.ring_volume_option_title; + mNormalIconId = R.drawable.ic_notifications; + mSilentIconId = R.drawable.ic_notifications_off_24dp; + } + // todo: set a distinct vibrate icon for ring vs notification + mVibrateIconId = R.drawable.ic_volume_ringer_vibrate; + } + + /** + * As the responsibility of this slider changes, so should its title & icon + */ + public void onDeviceConfigChange(DeviceConfig.Properties properties) { + Set changeSet = properties.getKeyset(); + if (changeSet.contains(SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION)) { + boolean valueUpdated = readSeparateNotificationVolumeConfig(); + if (valueUpdated) { + updateEffectsSuppressor(); + selectPreferenceIconState(); + setPreferenceTitle(); + } + } + } + + /** + * side effect: updates the cached value of the config, and also the icon + * @return has the config changed? + */ + private boolean readSeparateNotificationVolumeConfig() { + boolean newVal = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, CONFIG_DEFAULT_VAL); + + boolean valueUpdated = newVal != mSeparateNotification; + if (valueUpdated) { + mSeparateNotification = newVal; + loadPreferenceIconResources(newVal); + } + + return valueUpdated; } @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @@ -117,8 +145,11 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr public void onResume() { super.onResume(); mReceiver.register(true); + readSeparateNotificationVolumeConfig(); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, + ActivityThread.currentApplication().getMainExecutor(), this::onDeviceConfigChange); updateEffectsSuppressor(); - updatePreferenceIcon(); + selectPreferenceIconState(); setPreferenceTitle(); } @@ -127,6 +158,7 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr public void onPause() { super.onPause(); mReceiver.register(false); + DeviceConfig.removeOnPropertiesChangedListener(this::onDeviceConfigChange); } @Override @@ -170,7 +202,7 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr final int ringerMode = mHelper.getRingerModeInternal(); if (mRingerMode == ringerMode) return; mRingerMode = ringerMode; - updatePreferenceIcon(); + selectPreferenceIconState(); } private void updateEffectsSuppressor() { @@ -190,7 +222,8 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr return; } - if (hintsMatch(hints, mRingAliasNotif)) { + if (hintsMatch(hints, DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, false))) { mSuppressor = suppressor; if (mPreference != null) { final String text = SuppressorHelper.getSuppressionText(mContext, suppressor); @@ -200,11 +233,11 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr } @VisibleForTesting - boolean hintsMatch(int hints, boolean ringNotificationAliased) { + boolean hintsMatch(int hints, boolean notificationSeparated) { return (hints & NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS) != 0 || (hints & NotificationListenerService.HINT_HOST_DISABLE_EFFECTS) != 0 || ((hints & NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS) - != 0 && ringNotificationAliased); + != 0 && !notificationSeparated); } @VisibleForTesting @@ -217,7 +250,7 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr mVibrator = vibrator; } - private void updatePreferenceIcon() { + private void selectPreferenceIconState() { if (mPreference != null) { if (mRingerMode == AudioManager.RINGER_MODE_NORMAL) { mPreference.showIcon(mNormalIconId); diff --git a/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java b/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java index d1701599c34..0414565721e 100644 --- a/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java +++ b/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java @@ -55,13 +55,17 @@ public abstract class VolumeSeekBarPreferenceController extends public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); if (isAvailable()) { - mPreference = screen.findPreference(getPreferenceKey()); - mPreference.setCallback(mVolumePreferenceCallback); - mPreference.setStream(getAudioStream()); - mPreference.setMuteIcon(getMuteIcon()); + setupVolPreference(screen); } } + protected void setupVolPreference(PreferenceScreen screen) { + mPreference = screen.findPreference(getPreferenceKey()); + mPreference.setCallback(mVolumePreferenceCallback); + mPreference.setStream(getAudioStream()); + mPreference.setMuteIcon(getMuteIcon()); + } + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) public void onResume() { if (mPreference != null) { diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt index e3d0805434b..3a4d3f6529e 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt @@ -98,7 +98,7 @@ private fun AppInfoSettings(packageInfoPresenter: PackageInfoPresenter) { // TODO: notification_settings AppPermissionPreference(app) AppStoragePreference(app) - // TODO: instant_app_launch_supported_domain_urls + InstantAppDomainsPreference(app) AppDataUsagePreference(app) AppTimeSpentPreference(app) AppBatteryPreference(app) diff --git a/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt b/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt new file mode 100644 index 00000000000..3a7d50d24c9 --- /dev/null +++ b/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreference.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.spa.app.appinfo + +import android.content.Context +import android.content.pm.ApplicationInfo +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.android.settings.R +import com.android.settings.Utils +import com.android.settingslib.spa.framework.compose.collectAsStateWithLifecycle +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spaprivileged.framework.common.asUser +import com.android.settingslib.spaprivileged.model.app.userHandle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +@Composable +fun InstantAppDomainsPreference(app: ApplicationInfo) { + val context = LocalContext.current + if (!app.isInstantApp) return + + val presenter = remember { InstantAppDomainsPresenter(context, app) } + var openDialog by rememberSaveable { mutableStateOf(false) } + + Preference(object : PreferenceModel { + override val title = stringResource(R.string.app_launch_supported_domain_urls_title) + override val summary = presenter.summaryFlow.collectAsStateWithLifecycle( + initialValue = stringResource(R.string.summary_placeholder), + ) + override val onClick = { openDialog = true } + }) + + val domainsState = presenter.domainsFlow.collectAsStateWithLifecycle(initialValue = emptySet()) + if (openDialog) { + Dialog(domainsState) { + openDialog = false + } + } +} + +@Composable +private fun Dialog(domainsState: State>, onDismissRequest: () -> Unit) { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = {}, + title = { + Text(stringResource(R.string.app_launch_supported_domain_urls_title)) + }, + text = { + Column { + domainsState.value.forEach { domain -> + Text( + text = domain, + modifier = Modifier.padding(vertical = SettingsDimension.itemPaddingAround), + ) + } + } + }, + ) +} + +private class InstantAppDomainsPresenter( + private val context: Context, + private val app: ApplicationInfo, +) { + private val userContext = context.asUser(app.userHandle) + private val userPackageManager = userContext.packageManager + + val domainsFlow = flow { + emit(Utils.getHandledDomains(userPackageManager, app.packageName)) + }.flowOn(Dispatchers.IO) + + val summaryFlow = domainsFlow.map { entries -> + when (entries.size) { + 0 -> context.getString(R.string.domain_urls_summary_none) + 1 -> context.getString(R.string.domain_urls_summary_one, entries.first()) + else -> context.getString(R.string.domain_urls_summary_some, entries.first()) + } + }.flowOn(Dispatchers.IO) +} diff --git a/tests/robotests/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceControllerTest.java index 7ab311fe2ab..72477b94585 100644 --- a/tests/robotests/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceControllerTest.java @@ -24,9 +24,9 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothManager; import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; import android.os.SystemProperties; @@ -54,11 +54,6 @@ public class BluetoothMaxConnectedAudioDevicesPreferenceControllerTest { @Spy private Context mSpyContext = RuntimeEnvironment.application; - @Mock - private BluetoothManager mBluetoothManager; - @Mock - private BluetoothAdapter mBluetoothAdapter; - private ListPreference mPreference; private BluetoothMaxConnectedAudioDevicesPreferenceController mController; @@ -68,15 +63,19 @@ public class BluetoothMaxConnectedAudioDevicesPreferenceControllerTest { @Before public void setup() { MockitoAnnotations.initMocks(this); - doReturn(mBluetoothManager).when(mSpyContext).getSystemService(BluetoothManager.class); - doReturn(mBluetoothAdapter).when(mBluetoothManager).getAdapter(); // Get XML values without mock // Setup test list preference using XML values mPreference = new ListPreference(mSpyContext); mPreference.setEntries(R.array.bluetooth_max_connected_audio_devices); mPreference.setEntryValues(R.array.bluetooth_max_connected_audio_devices_values); - doReturn(TEST_MAX_CONNECTED_AUDIO_DEVICES).when(mBluetoothAdapter) - .getMaxConnectedAudioDevices(); + // Retrieve default max connected audio devices to a test controlled value + try { + Resources res = mSpyContext.getPackageManager().getResourcesForApplication("com.android.bluetooth"); + TEST_MAX_CONNECTED_AUDIO_DEVICES = res.getInteger(res.getIdentifier("config_bluetooth_max_connected_audio_devices", "integer", "com.android.bluetooth")); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + // Init the actual controller mController = new BluetoothMaxConnectedAudioDevicesPreferenceController(mSpyContext); // Construct preference in the controller via a mocked preference screen object diff --git a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java index 45e80891faa..2245920d046 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java @@ -280,8 +280,7 @@ public class AdvancedPowerUsageDetailTest { ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); verify(mEntityHeaderController).setSummary(captor.capture()); - assertThat(captor.getValue().toString()) - .isEqualTo("No usage from last full charge"); + assertThat(captor.getValue().toString()).isEmpty(); } @Test @@ -571,7 +570,8 @@ public class AdvancedPowerUsageDetailTest { public void startBatteryDetailPage_noBatteryUsage_hasBasicData() { final ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); - AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment, PACKAGE_NAME[0]); + AdvancedPowerUsageDetail.startBatteryDetailPage( + mActivity, mFragment, PACKAGE_NAME[0], UserHandle.OWNER); verify(mActivity).startActivity(captor.capture()); @@ -589,7 +589,8 @@ public class AdvancedPowerUsageDetailTest { PackageManager.NameNotFoundException { doReturn(UID).when(mPackageManager).getPackageUid(PACKAGE_NAME[0], 0 /* no flag */); - AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment, PACKAGE_NAME[0]); + AdvancedPowerUsageDetail.startBatteryDetailPage( + mActivity, mFragment, PACKAGE_NAME[0], UserHandle.OWNER); assertThat(mBundle.getInt(AdvancedPowerUsageDetail.EXTRA_UID)).isEqualTo(UID); } diff --git a/tests/robotests/src/com/android/settings/notification/NotificationVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/NotificationVolumePreferenceControllerTest.java index 96b9e6219c8..7e7ad10d8c1 100644 --- a/tests/robotests/src/com/android/settings/notification/NotificationVolumePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/NotificationVolumePreferenceControllerTest.java @@ -18,6 +18,7 @@ package com.android.settings.notification; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -25,10 +26,17 @@ import android.content.Context; import android.content.res.Resources; import android.media.AudioManager; import android.os.Vibrator; +import android.provider.DeviceConfig; import android.service.notification.NotificationListenerService; import android.telephony.TelephonyManager; -import com.android.internal.R; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.testutils.shadow.ShadowDeviceConfig; import org.junit.Before; import org.junit.Test; @@ -37,11 +45,12 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.Shadows; import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowDeviceConfig.class}) public class NotificationVolumePreferenceControllerTest { - @Mock private AudioHelper mHelper; @Mock @@ -52,6 +61,11 @@ public class NotificationVolumePreferenceControllerTest { private Vibrator mVibrator; @Mock private Resources mResources; + @Mock + private PreferenceManager mPreferenceManager; + + private static final String READ_DEVICE_CONFIG_PERMISSION = + "android.permission.READ_DEVICE_CONFIG"; private Context mContext; private NotificationVolumePreferenceController mController; @@ -87,7 +101,9 @@ public class NotificationVolumePreferenceControllerTest { public void isAvailable_voiceCapable_aliasedWithRing_shouldReturnFalse() { when(mResources.getBoolean( com.android.settings.R.bool.config_show_notification_volume)).thenReturn(true); - when(mResources.getBoolean(R.bool.config_alias_ring_notif_stream_types)).thenReturn(true); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false); NotificationVolumePreferenceController controller = new NotificationVolumePreferenceController(mContext); @@ -105,7 +121,9 @@ public class NotificationVolumePreferenceControllerTest { public void isAvailable_voiceCapable_separatedFromRing_shouldReturnTrue() { when(mResources.getBoolean( com.android.settings.R.bool.config_show_notification_volume)).thenReturn(true); - when(mResources.getBoolean(R.bool.config_alias_ring_notif_stream_types)).thenReturn(false); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "true", false); NotificationVolumePreferenceController controller = new NotificationVolumePreferenceController(mContext); @@ -170,4 +188,70 @@ public class NotificationVolumePreferenceControllerTest { .isTrue(); } + @Test + public void enableSeparateNotificationConfig_controllerBecomesAvailable() { + PreferenceScreen screen = spy(new PreferenceScreen(mContext, null)); + VolumeSeekBarPreference volumeSeekBarPreference = mock(VolumeSeekBarPreference.class); + when(screen.getPreferenceManager()).thenReturn(mPreferenceManager); + when(screen.getContext()).thenReturn(mContext); + when(mResources.getBoolean( + com.android.settings.R.bool.config_show_notification_volume)).thenReturn(true); + // block the alternative condition to enable controller + when(mTelephonyManager.isVoiceCapable()).thenReturn(true); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false); + + NotificationVolumePreferenceController controller = + new NotificationVolumePreferenceController(mContext); + when(screen.findPreference(controller.getPreferenceKey())) + .thenReturn(volumeSeekBarPreference); + + // allow the controller to subscribe + Shadows.shadowOf((android.app.Application) ApplicationProvider.getApplicationContext()) + .grantPermissions(READ_DEVICE_CONFIG_PERMISSION); + controller.onResume(); + controller.displayPreference(screen); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, Boolean.toString(true), + false); + + assertThat(controller.getAvailabilityStatus() + == BasePreferenceController.AVAILABLE).isTrue(); + } + + @Test + public void disableSeparateNotificationConfig_controllerBecomesUnavailable() { + PreferenceScreen screen = spy(new PreferenceScreen(mContext, null)); + VolumeSeekBarPreference volumeSeekBarPreference = mock(VolumeSeekBarPreference.class); + when(screen.getPreferenceManager()).thenReturn(mPreferenceManager); + when(screen.getContext()).thenReturn(mContext); + when(mResources.getBoolean( + com.android.settings.R.bool.config_show_notification_volume)).thenReturn(true); + + // block the alternative condition to enable controller + when(mTelephonyManager.isVoiceCapable()).thenReturn(true); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "true", false); + + NotificationVolumePreferenceController controller = + new NotificationVolumePreferenceController(mContext); + + when(screen.findPreference(controller.getPreferenceKey())) + .thenReturn(volumeSeekBarPreference); + + Shadows.shadowOf((android.app.Application) ApplicationProvider.getApplicationContext()) + .grantPermissions(READ_DEVICE_CONFIG_PERMISSION); + controller.onResume(); + controller.displayPreference(screen); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false); + + assertThat(controller.getAvailabilityStatus() + == BasePreferenceController.UNSUPPORTED_ON_DEVICE).isTrue(); + } + } diff --git a/tests/robotests/src/com/android/settings/notification/RingVolumePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/RingVolumePreferenceControllerTest.java index 02757d52874..1ad26c71546 100644 --- a/tests/robotests/src/com/android/settings/notification/RingVolumePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/RingVolumePreferenceControllerTest.java @@ -27,10 +27,13 @@ import android.content.Context; import android.content.res.Resources; import android.media.AudioManager; import android.os.Vibrator; +import android.provider.DeviceConfig; import android.service.notification.NotificationListenerService; import android.telephony.TelephonyManager; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.settings.R; +import com.android.settings.testutils.shadow.ShadowDeviceConfig; import org.junit.Before; import org.junit.Test; @@ -39,9 +42,11 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowApplication; @RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowDeviceConfig.class}) public class RingVolumePreferenceControllerTest { @Mock @@ -124,9 +129,10 @@ public class RingVolumePreferenceControllerTest { // todo: verify that the title change is displayed, by examining the underlying preference @Test public void ringNotificationStreamsNotAliased_sliderTitleSetToRingOnly() { - when(mResources.getBoolean( - com.android.internal.R.bool.config_alias_ring_notif_stream_types)) - .thenReturn(false); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "true", false); + final RingVolumePreferenceController controller = new RingVolumePreferenceController(mContext); @@ -138,8 +144,9 @@ public class RingVolumePreferenceControllerTest { @Test public void ringNotificationStreamsAliased_sliderTitleIncludesBothRingNotification() { - when(mResources.getBoolean( - com.android.internal.R.bool.config_alias_ring_notif_stream_types)).thenReturn(true); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.VOLUME_SEPARATE_NOTIFICATION, "false", false); + final RingVolumePreferenceController control = new RingVolumePreferenceController(mContext); int expectedTitleId = R.string.ring_volume_option_title; @@ -150,39 +157,39 @@ public class RingVolumePreferenceControllerTest { @Test public void setHintsRing_aliased_Matches() { assertThat(mController.hintsMatch( - NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS, true)).isTrue(); + NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS, false)).isTrue(); } @Test public void setHintsRingNotification_aliased_Matches() { assertThat(mController.hintsMatch(NotificationListenerService.HINT_HOST_DISABLE_EFFECTS, - true)).isTrue(); + false)).isTrue(); } @Test public void setHintNotification_aliased_Matches() { assertThat(mController .hintsMatch(NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS, - true)).isTrue(); + false)).isTrue(); } @Test public void setHintsRing_unaliased_Matches() { assertThat(mController.hintsMatch( - NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS, false)).isTrue(); + NotificationListenerService.HINT_HOST_DISABLE_CALL_EFFECTS, true)).isTrue(); } @Test public void setHintsRingNotification_unaliased_Matches() { assertThat(mController.hintsMatch(NotificationListenerService.HINT_HOST_DISABLE_EFFECTS, - false)).isTrue(); + true)).isTrue(); } @Test public void setHintNotification_unaliased_doesNotMatch() { assertThat(mController .hintsMatch(NotificationListenerService.HINT_HOST_DISABLE_NOTIFICATION_EFFECTS, - false)).isFalse(); + true)).isFalse(); } @Test diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt new file mode 100644 index 00000000000..9782817f742 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/InstantAppDomainsPreferenceTest.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.spa.app.appinfo + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.filterToOne +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isDialog +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.settings.R +import com.android.settings.Utils +import com.android.settings.testutils.delay +import org.junit.After +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.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.MockitoSession +import org.mockito.Spy +import org.mockito.quality.Strictness +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class InstantAppDomainsPreferenceTest { + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var mockSession: MockitoSession + + @Spy + private val context: Context = ApplicationProvider.getApplicationContext() + + @Mock + private lateinit var packageManager: PackageManager + + @Before + fun setUp() { + mockSession = ExtendedMockito.mockitoSession() + .initMocks(this) + .mockStatic(Utils::class.java) + .strictness(Strictness.LENIENT) + .startMocking() + whenever(context.packageManager).thenReturn(packageManager) + Mockito.doReturn(context).`when`(context).createContextAsUser(any(), anyInt()) + mockDomains(emptySet()) + } + + @After + fun tearDown() { + mockSession.finishMocking() + } + + private fun mockDomains(domains: Set) { + whenever(Utils.getHandledDomains(packageManager, PACKAGE_NAME)).thenReturn(domains) + } + + @Test + fun notInstantApp_notDisplayed() { + val app = ApplicationInfo() + + setContent(app) + + composeTestRule.onRoot().assertIsNotDisplayed() + } + + @Test + fun title_displayed() { + setContent() + + composeTestRule + .onNodeWithText(context.getString(R.string.app_launch_supported_domain_urls_title)) + .assertIsDisplayed() + .assertIsEnabled() + } + + @Test + fun noDomain() { + mockDomains(emptySet()) + + setContent() + + composeTestRule.onNodeWithText(context.getString(R.string.domain_urls_summary_none)) + .assertIsDisplayed() + } + + @Test + fun oneDomain() { + mockDomains(setOf("abc")) + + setContent() + + composeTestRule.onNodeWithText("Open abc").assertIsDisplayed() + } + + @Test + fun twoDomains() { + mockDomains(setOf("abc", "def")) + + setContent() + + composeTestRule.onNodeWithText("Open abc and other URLs").assertIsDisplayed() + } + + @Test + fun whenClicked() { + mockDomains(setOf("abc", "def")) + + setContent() + composeTestRule.onRoot().performClick() + composeTestRule.delay() + + assertDialogHasText(context.getString(R.string.app_launch_supported_domain_urls_title)) + assertDialogHasText("abc") + assertDialogHasText("def") + } + + private fun assertDialogHasText(text: String) { + composeTestRule.onAllNodes(hasAnyAncestor(isDialog())) + .filterToOne(hasText(text)) + .assertIsDisplayed() + } + + private fun setContent(app:ApplicationInfo = INSTANT_APP) { + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + InstantAppDomainsPreference(app) + } + } + } + + private companion object { + const val PACKAGE_NAME = "package.name" + const val UID = 123 + + val INSTANT_APP = ApplicationInfo().apply { + packageName = PACKAGE_NAME + uid = UID + privateFlags = ApplicationInfo.PRIVATE_FLAG_INSTANT + } + } +}