Merge "Fix missing DesktopWallpaperActivity in DW" into main
diff --git a/SQLITE_OWNERS b/SQLITE_OWNERS
index 1ff72e7..783a0b6 100644
--- a/SQLITE_OWNERS
+++ b/SQLITE_OWNERS
@@ -1,2 +1,9 @@
+# Android platform SQLite owners are responsible for:
+# 1. Periodically updating libsqlite from upstream sqlite.org.
+# 2. Escalating libsqlite bug reports to upstream sqlite.org.
+# 3. Addressing bugs, performance regressions, and feature requests
+# in Android SDK SQLite wrappers (android.database.sqlite.*).
+# 4. Reviewing proposed changes to said Android SDK SQLite wrappers.
+
shayba@google.com
shombert@google.com
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index d6425c3..ed20207 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -32,3 +32,14 @@
description: "Provides additional callbacks with information about user actions in ChooserResult"
bug: "263474465"
}
+
+flag {
+ name: "fix_resolver_memory_leak"
+ is_exported: true
+ namespace: "intentresolver"
+ description: "ResolverActivity memory leak (through the AppPredictor callback) fix"
+ bug: "346671041"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 920981e..a194535 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -1209,9 +1209,19 @@
if (!isChangingConfigurations() && mPickOptionRequest != null) {
mPickOptionRequest.cancel();
}
- if (mMultiProfilePagerAdapter != null
- && mMultiProfilePagerAdapter.getActiveListAdapter() != null) {
- mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy();
+ if (mMultiProfilePagerAdapter != null) {
+ ResolverListAdapter activeAdapter =
+ mMultiProfilePagerAdapter.getActiveListAdapter();
+ if (activeAdapter != null) {
+ activeAdapter.onDestroy();
+ }
+ if (android.service.chooser.Flags.fixResolverMemoryLeak()) {
+ ResolverListAdapter inactiveAdapter =
+ mMultiProfilePagerAdapter.getInactiveListAdapter();
+ if (inactiveAdapter != null) {
+ inactiveAdapter.onDestroy();
+ }
+ }
}
}
diff --git a/packages/SettingsLib/res/drawable/ic_do_not_disturb_on_24dp.xml b/packages/SettingsLib/res/drawable/ic_do_not_disturb_on_24dp.xml
new file mode 100644
index 0000000..06c0d8c
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_do_not_disturb_on_24dp.xml
@@ -0,0 +1,28 @@
+<!--
+ 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorControlNormal">
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8c0,-4.41 3.59,-8 8,-8c4.41,0 8,3.59 8,8C20,16.41 16.41,20 12,20z"/>
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M7,11h10v2h-10z"/>
+</vector>
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index d373201..27c386e 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1361,6 +1361,9 @@
<!-- Keywords for setting screen for controlling apps that can schedule alarms [CHAR LIMIT=100] -->
<string name="keywords_alarms_and_reminders">schedule, alarm, reminder, clock</string>
+ <!-- Sound: Title for the Do not Disturb option and associated settings page. [CHAR LIMIT=50]-->
+ <string name="zen_mode_settings_title">Do Not Disturb</string>
+
<!-- Do not disturb: Label for button in enable zen dialog that will turn on zen mode. [CHAR LIMIT=30] -->
<string name="zen_mode_enable_dialog_turn_on">Turn on</string>
<!-- Do not disturb: Title for the Do not Disturb dialog to turn on Do not disturb. [CHAR LIMIT=50]-->
@@ -1387,6 +1390,9 @@
<!-- Do not disturb: Duration option to always have DND on until it is manually turned off [CHAR LIMIT=60] -->
<string name="zen_mode_forever">Until you turn off</string>
+ <!-- [CHAR LIMIT=50] Zen mode settings: placeholder for a Contact name when the name is empty -->
+ <string name="zen_mode_starred_contacts_empty_name">(No name)</string>
+
<!-- time label for event have that happened very recently [CHAR LIMIT=60] -->
<string name="time_unit_just_now">Just now</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java
new file mode 100644
index 0000000..3f19830
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenIconLoader.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.notification.modes;
+
+import static com.google.common.util.concurrent.Futures.immediateFuture;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.Nullable;
+import android.app.AutomaticZenRule;
+import android.content.Context;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.service.notification.SystemZenRules;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.LruCache;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.content.res.AppCompatResources;
+
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class ZenIconLoader {
+
+ private static final String TAG = "ZenIconLoader";
+
+ private static final Drawable MISSING = new ColorDrawable();
+
+ @Nullable // Until first usage
+ private static ZenIconLoader sInstance;
+
+ private final LruCache<String, Drawable> mCache;
+ private final ListeningExecutorService mBackgroundExecutor;
+
+ public static ZenIconLoader getInstance() {
+ if (sInstance == null) {
+ sInstance = new ZenIconLoader();
+ }
+ return sInstance;
+ }
+
+ private ZenIconLoader() {
+ this(Executors.newFixedThreadPool(4));
+ }
+
+ @VisibleForTesting
+ ZenIconLoader(ExecutorService backgroundExecutor) {
+ mCache = new LruCache<>(50);
+ mBackgroundExecutor =
+ MoreExecutors.listeningDecorator(backgroundExecutor);
+ }
+
+ @NonNull
+ ListenableFuture<Drawable> getIcon(Context context, @NonNull AutomaticZenRule rule) {
+ if (rule.getIconResId() == 0) {
+ return Futures.immediateFuture(getFallbackIcon(context, rule.getType()));
+ }
+
+ return FluentFuture.from(loadIcon(context, rule.getPackageName(), rule.getIconResId()))
+ .transform(icon ->
+ icon != null ? icon : getFallbackIcon(context, rule.getType()),
+ MoreExecutors.directExecutor());
+ }
+
+ @NonNull
+ private ListenableFuture</* @Nullable */ Drawable> loadIcon(Context context, String pkg,
+ int iconResId) {
+ String cacheKey = pkg + ":" + iconResId;
+ synchronized (mCache) {
+ Drawable cachedValue = mCache.get(cacheKey);
+ if (cachedValue != null) {
+ return immediateFuture(cachedValue != MISSING ? cachedValue : null);
+ }
+ }
+
+ return FluentFuture.from(mBackgroundExecutor.submit(() -> {
+ if (TextUtils.isEmpty(pkg) || SystemZenRules.PACKAGE_ANDROID.equals(pkg)) {
+ return context.getDrawable(iconResId);
+ } else {
+ Context appContext = context.createPackageContext(pkg, 0);
+ Drawable appDrawable = AppCompatResources.getDrawable(appContext, iconResId);
+ return getMonochromeIconIfPresent(appDrawable);
+ }
+ })).catching(Exception.class, ex -> {
+ // If we cannot resolve the icon, then store MISSING in the cache below, so
+ // we don't try again.
+ Log.e(TAG, "Error while loading icon " + cacheKey, ex);
+ return null;
+ }, MoreExecutors.directExecutor()).transform(drawable -> {
+ synchronized (mCache) {
+ mCache.put(cacheKey, drawable != null ? drawable : MISSING);
+ }
+ return drawable;
+ }, MoreExecutors.directExecutor());
+ }
+
+ private static Drawable getFallbackIcon(Context context, int ruleType) {
+ int iconResIdFromType = switch (ruleType) {
+ case AutomaticZenRule.TYPE_UNKNOWN ->
+ com.android.internal.R.drawable.ic_zen_mode_type_unknown;
+ case AutomaticZenRule.TYPE_OTHER ->
+ com.android.internal.R.drawable.ic_zen_mode_type_other;
+ case AutomaticZenRule.TYPE_SCHEDULE_TIME ->
+ com.android.internal.R.drawable.ic_zen_mode_type_schedule_time;
+ case AutomaticZenRule.TYPE_SCHEDULE_CALENDAR ->
+ com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar;
+ case AutomaticZenRule.TYPE_BEDTIME ->
+ com.android.internal.R.drawable.ic_zen_mode_type_bedtime;
+ case AutomaticZenRule.TYPE_DRIVING ->
+ com.android.internal.R.drawable.ic_zen_mode_type_driving;
+ case AutomaticZenRule.TYPE_IMMERSIVE ->
+ com.android.internal.R.drawable.ic_zen_mode_type_immersive;
+ case AutomaticZenRule.TYPE_THEATER ->
+ com.android.internal.R.drawable.ic_zen_mode_type_theater;
+ case AutomaticZenRule.TYPE_MANAGED ->
+ com.android.internal.R.drawable.ic_zen_mode_type_managed;
+ default -> com.android.internal.R.drawable.ic_zen_mode_type_unknown;
+ };
+ return requireNonNull(context.getDrawable(iconResIdFromType));
+ }
+
+ private static Drawable getMonochromeIconIfPresent(Drawable icon) {
+ // For created rules, the app should've provided a monochrome Drawable. However, implicit
+ // rules have the app's icon, which is not -- but might have a monochrome layer. Thus
+ // we choose it, if present.
+ if (icon instanceof AdaptiveIconDrawable adaptiveIcon) {
+ if (adaptiveIcon.getMonochrome() != null) {
+ // Wrap with negative inset => scale icon (inspired from BaseIconFactory)
+ return new InsetDrawable(adaptiveIcon.getMonochrome(),
+ -2.0f * AdaptiveIconDrawable.getExtraInsetFraction());
+ }
+ }
+ return icon;
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
new file mode 100644
index 0000000..598db2a9
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent;
+import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleTime;
+import static android.service.notification.ZenModeConfig.tryParseEventConditionId;
+import static android.service.notification.ZenModeConfig.tryParseScheduleConditionId;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.SuppressLint;
+import android.app.AutomaticZenRule;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.service.notification.SystemZenRules;
+import android.service.notification.ZenDeviceEffects;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.R;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Objects;
+
+/**
+ * Represents either an {@link AutomaticZenRule} or the manual DND rule in a unified way.
+ *
+ * <p>It also adapts other rule features that we don't want to expose in the UI, such as
+ * interruption filters other than {@code PRIORITY}, rules without specific icons, etc.
+ */
+public class ZenMode {
+
+ private static final String TAG = "ZenMode";
+
+ public static final String MANUAL_DND_MODE_ID = "manual_dnd";
+
+ // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
+ private static final ZenPolicy POLICY_INTERRUPTION_FILTER_ALARMS =
+ new ZenPolicy.Builder()
+ .disallowAllSounds()
+ .allowAlarms(true)
+ .allowMedia(true)
+ .allowPriorityChannels(false)
+ .build();
+
+ // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
+ private static final ZenPolicy POLICY_INTERRUPTION_FILTER_NONE =
+ new ZenPolicy.Builder()
+ .disallowAllSounds()
+ .hideAllVisualEffects()
+ .allowPriorityChannels(false)
+ .build();
+
+ private final String mId;
+ private AutomaticZenRule mRule;
+ private final boolean mIsActive;
+ private final boolean mIsManualDnd;
+
+ public ZenMode(String id, AutomaticZenRule rule, boolean isActive) {
+ this(id, rule, isActive, false);
+ }
+
+ private ZenMode(String id, AutomaticZenRule rule, boolean isActive, boolean isManualDnd) {
+ mId = id;
+ mRule = rule;
+ mIsActive = isActive;
+ mIsManualDnd = isManualDnd;
+ }
+
+ public static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
+ return new ZenMode(MANUAL_DND_MODE_ID, manualRule, isActive, true);
+ }
+
+ @NonNull
+ public String getId() {
+ return mId;
+ }
+
+ @NonNull
+ public AutomaticZenRule getRule() {
+ return mRule;
+ }
+
+ @NonNull
+ public ListenableFuture<Drawable> getIcon(@NonNull Context context,
+ @NonNull ZenIconLoader iconLoader) {
+ if (mIsManualDnd) {
+ return Futures.immediateFuture(requireNonNull(
+ context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
+ }
+
+ return iconLoader.getIcon(context, mRule);
+ }
+
+ @NonNull
+ public ZenPolicy getPolicy() {
+ switch (mRule.getInterruptionFilter()) {
+ case INTERRUPTION_FILTER_PRIORITY:
+ case NotificationManager.INTERRUPTION_FILTER_ALL:
+ return requireNonNull(mRule.getZenPolicy());
+
+ case NotificationManager.INTERRUPTION_FILTER_ALARMS:
+ return POLICY_INTERRUPTION_FILTER_ALARMS;
+
+ case NotificationManager.INTERRUPTION_FILTER_NONE:
+ return POLICY_INTERRUPTION_FILTER_NONE;
+
+ case NotificationManager.INTERRUPTION_FILTER_UNKNOWN:
+ default:
+ Log.wtf(TAG, "Rule " + mId + " with unexpected interruptionFilter "
+ + mRule.getInterruptionFilter());
+ return requireNonNull(mRule.getZenPolicy());
+ }
+ }
+
+ /**
+ * Updates the {@link ZenPolicy} of the associated {@link AutomaticZenRule} based on the
+ * supplied policy. In some cases this involves conversions, so that the following call
+ * to {@link #getPolicy} might return a different policy from the one supplied here.
+ */
+ @SuppressLint("WrongConstant")
+ public void setPolicy(@NonNull ZenPolicy policy) {
+ ZenPolicy currentPolicy = getPolicy();
+ if (currentPolicy.equals(policy)) {
+ return;
+ }
+
+ if (mRule.getInterruptionFilter() == INTERRUPTION_FILTER_ALL) {
+ Log.wtf(TAG, "Able to change policy without filtering being enabled");
+ }
+
+ // If policy is customized from any of the "special" ones, make the rule PRIORITY.
+ if (mRule.getInterruptionFilter() != INTERRUPTION_FILTER_PRIORITY) {
+ mRule.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY);
+ }
+ mRule.setZenPolicy(policy);
+ }
+
+ @NonNull
+ public ZenDeviceEffects getDeviceEffects() {
+ return mRule.getDeviceEffects() != null
+ ? mRule.getDeviceEffects()
+ : new ZenDeviceEffects.Builder().build();
+ }
+
+ public void setCustomModeConditionId(Context context, Uri conditionId) {
+ checkState(SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName()),
+ "Trying to change condition of non-system-owned rule %s (to %s)",
+ mRule, conditionId);
+
+ Uri oldCondition = mRule.getConditionId();
+ mRule.setConditionId(conditionId);
+
+ ZenModeConfig.ScheduleInfo scheduleInfo = tryParseScheduleConditionId(conditionId);
+ if (scheduleInfo != null) {
+ mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_TIME);
+ mRule.setOwner(ZenModeConfig.getScheduleConditionProvider());
+ mRule.setTriggerDescription(
+ getTriggerDescriptionForScheduleTime(context, scheduleInfo));
+ return;
+ }
+
+ ZenModeConfig.EventInfo eventInfo = tryParseEventConditionId(conditionId);
+ if (eventInfo != null) {
+ mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR);
+ mRule.setOwner(ZenModeConfig.getEventConditionProvider());
+ mRule.setTriggerDescription(getTriggerDescriptionForScheduleEvent(context, eventInfo));
+ return;
+ }
+
+ if (ZenModeConfig.isValidCustomManualConditionId(conditionId)) {
+ mRule.setType(AutomaticZenRule.TYPE_OTHER);
+ mRule.setOwner(ZenModeConfig.getCustomManualConditionProvider());
+ mRule.setTriggerDescription("");
+ return;
+ }
+
+ Log.wtf(TAG, String.format(
+ "Changed condition of rule %s (%s -> %s) but cannot recognize which kind of "
+ + "condition it was!",
+ mRule, oldCondition, conditionId));
+ }
+
+ public boolean canEditName() {
+ return !isManualDnd();
+ }
+
+ public boolean canEditIcon() {
+ return !isManualDnd();
+ }
+
+ public boolean canBeDeleted() {
+ return !isManualDnd();
+ }
+
+ public boolean isManualDnd() {
+ return mIsManualDnd;
+ }
+
+ public boolean isActive() {
+ return mIsActive;
+ }
+
+ public boolean isSystemOwned() {
+ return SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName());
+ }
+
+ @AutomaticZenRule.Type
+ public int getType() {
+ return mRule.getType();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ return obj instanceof ZenMode other
+ && mId.equals(other.mId)
+ && mRule.equals(other.mRule)
+ && mIsActive == other.mIsActive;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mId, mRule, mIsActive);
+ }
+
+ @Override
+ public String toString() {
+ return mId + "(" + (mIsActive ? "active" : "inactive") + ") -> " + mRule;
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java
new file mode 100644
index 0000000..d07d743
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.notification.modes;
+
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.AutomaticZenRule;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.Settings;
+import android.service.notification.Condition;
+import android.service.notification.ZenModeConfig;
+import android.util.Log;
+
+import com.android.settingslib.R;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class used for Settings-NMS interactions related to Mode management.
+ *
+ * <p>This class converts {@link AutomaticZenRule} instances, as well as the manual zen mode,
+ * into the unified {@link ZenMode} format.
+ */
+public class ZenModesBackend {
+
+ private static final String TAG = "ZenModeBackend";
+
+ @Nullable // Until first usage
+ private static ZenModesBackend sInstance;
+
+ private final NotificationManager mNotificationManager;
+
+ private final Context mContext;
+
+ public static ZenModesBackend getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new ZenModesBackend(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ ZenModesBackend(Context context) {
+ mContext = context;
+ mNotificationManager = context.getSystemService(NotificationManager.class);
+ }
+
+ public List<ZenMode> getModes() {
+ ArrayList<ZenMode> modes = new ArrayList<>();
+ ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
+ modes.add(getManualDndMode(currentConfig));
+
+ Map<String, AutomaticZenRule> zenRules = mNotificationManager.getAutomaticZenRules();
+ for (Map.Entry<String, AutomaticZenRule> zenRuleEntry : zenRules.entrySet()) {
+ String ruleId = zenRuleEntry.getKey();
+ modes.add(new ZenMode(ruleId, zenRuleEntry.getValue(),
+ isRuleActive(ruleId, currentConfig)));
+ }
+
+ modes.sort((l, r) -> {
+ if (l.isManualDnd()) {
+ return -1;
+ } else if (r.isManualDnd()) {
+ return 1;
+ }
+ return l.getRule().getName().compareTo(r.getRule().getName());
+ });
+
+ return modes;
+ }
+
+ @Nullable
+ public ZenMode getMode(String id) {
+ ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
+ if (ZenMode.MANUAL_DND_MODE_ID.equals(id)) {
+ return getManualDndMode(currentConfig);
+ } else {
+ AutomaticZenRule rule = mNotificationManager.getAutomaticZenRule(id);
+ if (rule == null) {
+ return null;
+ }
+ return new ZenMode(id, rule, isRuleActive(id, currentConfig));
+ }
+ }
+
+ private ZenMode getManualDndMode(ZenModeConfig config) {
+ ZenModeConfig.ZenRule manualRule = config.manualRule;
+ // TODO: b/333682392 - Replace with final strings for name & trigger description
+ AutomaticZenRule manualDndRule = new AutomaticZenRule.Builder(
+ mContext.getString(R.string.zen_mode_settings_title), manualRule.conditionId)
+ .setType(manualRule.type)
+ .setZenPolicy(manualRule.zenPolicy)
+ .setDeviceEffects(manualRule.zenDeviceEffects)
+ .setManualInvocationAllowed(manualRule.allowManualInvocation)
+ .setConfigurationActivity(null) // No further settings
+ .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
+ .build();
+
+ return ZenMode.manualDndMode(manualDndRule, config != null && config.isManualActive());
+ }
+
+ private static boolean isRuleActive(String id, ZenModeConfig config) {
+ if (config == null) {
+ // shouldn't happen if the config is coming from NM, but be safe
+ return false;
+ }
+ ZenModeConfig.ZenRule configRule = config.automaticRules.get(id);
+ return configRule != null && configRule.isAutomaticActive();
+ }
+
+ public void updateMode(ZenMode mode) {
+ if (mode.isManualDnd()) {
+ try {
+ NotificationManager.Policy dndPolicy =
+ new ZenModeConfig().toNotificationPolicy(mode.getPolicy());
+ mNotificationManager.setNotificationPolicy(dndPolicy, /* fromUser= */ true);
+
+ mNotificationManager.setManualZenRuleDeviceEffects(
+ mode.getRule().getDeviceEffects());
+ } catch (Exception e) {
+ Log.w(TAG, "Error updating manual mode", e);
+ }
+ } else {
+ mNotificationManager.updateAutomaticZenRule(mode.getId(), mode.getRule(),
+ /* fromUser= */ true);
+ }
+ }
+
+ public void activateMode(ZenMode mode, @Nullable Duration forDuration) {
+ if (mode.isManualDnd()) {
+ Uri durationConditionId = null;
+ if (forDuration != null) {
+ durationConditionId = ZenModeConfig.toTimeCondition(mContext,
+ (int) forDuration.toMinutes(), ActivityManager.getCurrentUser(), true).id;
+ }
+ mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
+ durationConditionId, TAG, /* fromUser= */ true);
+
+ } else {
+ if (forDuration != null) {
+ throw new IllegalArgumentException(
+ "Only the manual DND mode can be activated for a specific duration");
+ }
+ mNotificationManager.setAutomaticZenRuleState(mode.getId(),
+ new Condition(mode.getRule().getConditionId(), "", Condition.STATE_TRUE,
+ Condition.SOURCE_USER_ACTION));
+ }
+ }
+
+ public void deactivateMode(ZenMode mode) {
+ if (mode.isManualDnd()) {
+ // When calling with fromUser=true this will not snooze other modes.
+ mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_OFF, null, TAG,
+ /* fromUser= */ true);
+ } else {
+ // TODO: b/333527800 - This should (potentially) snooze the rule if it was active.
+ mNotificationManager.setAutomaticZenRuleState(mode.getId(),
+ new Condition(mode.getRule().getConditionId(), "", Condition.STATE_FALSE,
+ Condition.SOURCE_USER_ACTION));
+ }
+ }
+
+ public void removeMode(ZenMode mode) {
+ if (!mode.canBeDeleted()) {
+ throw new IllegalArgumentException("Mode " + mode + " cannot be deleted!");
+ }
+ mNotificationManager.removeAutomaticZenRule(mode.getId(), /* fromUser= */ true);
+ }
+
+ /**
+ * Creates a new custom mode with the provided {@code name}. The mode will be "manual" (i.e.
+ * not have a schedule), this can be later updated by the user in the mode settings page.
+ *
+ * @return the created mode. Only {@code null} if creation failed due to an internal error
+ */
+ @Nullable
+ public ZenMode addCustomMode(String name) {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder(name,
+ ZenModeConfig.toCustomManualConditionId())
+ .setPackage(ZenModeConfig.getCustomManualConditionProvider().getPackageName())
+ .setType(AutomaticZenRule.TYPE_OTHER)
+ .setOwner(ZenModeConfig.getCustomManualConditionProvider())
+ .setManualInvocationAllowed(true)
+ .build();
+
+ String ruleId = mNotificationManager.addAutomaticZenRule(rule);
+ return getMode(ruleId);
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java
new file mode 100644
index 0000000..20461e3
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenIconLoaderTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AutomaticZenRule;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.service.notification.ZenPolicy;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenIconLoaderTest {
+
+ private Context mContext;
+ private ZenIconLoader mLoader;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ mLoader = new ZenIconLoader(MoreExecutors.newDirectExecutorService());
+ }
+
+ @Test
+ public void getIcon_systemOwnedRuleWithIcon_loads() throws Exception {
+ AutomaticZenRule systemRule = newRuleBuilder()
+ .setPackage("android")
+ .setIconResId(android.R.drawable.ic_media_play)
+ .build();
+
+ ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, systemRule);
+ assertThat(loadFuture.isDone()).isTrue();
+ assertThat(loadFuture.get()).isNotNull();
+ }
+
+ @Test
+ public void getIcon_ruleWithoutSpecificIcon_loadsFallback() throws Exception {
+ AutomaticZenRule rule = newRuleBuilder()
+ .setType(AutomaticZenRule.TYPE_DRIVING)
+ .setPackage("com.blah")
+ .build();
+
+ ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, rule);
+ assertThat(loadFuture.isDone()).isTrue();
+ assertThat(loadFuture.get()).isNotNull();
+ }
+
+ @Test
+ public void getIcon_ruleWithAppIconWithLoadFailure_loadsFallback() throws Exception {
+ AutomaticZenRule rule = newRuleBuilder()
+ .setType(AutomaticZenRule.TYPE_DRIVING)
+ .setPackage("com.blah")
+ .setIconResId(-123456)
+ .build();
+
+ ListenableFuture<Drawable> loadFuture = mLoader.getIcon(mContext, rule);
+ assertThat(loadFuture.get()).isNotNull();
+ }
+
+ private static AutomaticZenRule.Builder newRuleBuilder() {
+ return new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().build());
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
new file mode 100644
index 0000000..43aba45
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AutomaticZenRule;
+import android.net.Uri;
+import android.service.notification.ZenPolicy;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ZenModeTest {
+
+ private static final ZenPolicy ZEN_POLICY = new ZenPolicy.Builder().allowAllSounds().build();
+
+ private static final AutomaticZenRule ZEN_RULE =
+ new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+ .setType(AutomaticZenRule.TYPE_DRIVING)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(ZEN_POLICY)
+ .build();
+
+ @Test
+ public void testBasicMethods() {
+ ZenMode zenMode = new ZenMode("id", ZEN_RULE, true);
+
+ assertThat(zenMode.getId()).isEqualTo("id");
+ assertThat(zenMode.getRule()).isEqualTo(ZEN_RULE);
+ assertThat(zenMode.isManualDnd()).isFalse();
+ assertThat(zenMode.canBeDeleted()).isTrue();
+ assertThat(zenMode.isActive()).isTrue();
+
+ ZenMode manualMode = ZenMode.manualDndMode(ZEN_RULE, false);
+ assertThat(manualMode.getId()).isEqualTo(ZenMode.MANUAL_DND_MODE_ID);
+ assertThat(manualMode.isManualDnd()).isTrue();
+ assertThat(manualMode.canBeDeleted()).isFalse();
+ assertThat(manualMode.isActive()).isFalse();
+ }
+
+ @Test
+ public void getPolicy_interruptionFilterPriority_returnsZenPolicy() {
+ ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(ZEN_POLICY)
+ .build(), false);
+
+ assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY);
+ }
+
+ @Test
+ public void getPolicy_interruptionFilterAlarms_returnsPolicyAllowingAlarms() {
+ ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .setZenPolicy(ZEN_POLICY) // should be ignored
+ .build(), false);
+
+ assertThat(zenMode.getPolicy()).isEqualTo(
+ new ZenPolicy.Builder()
+ .disallowAllSounds()
+ .allowAlarms(true)
+ .allowMedia(true)
+ .allowPriorityChannels(false)
+ .build());
+ }
+
+ @Test
+ public void getPolicy_interruptionFilterNone_returnsPolicyAllowingNothing() {
+ ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
+ .setZenPolicy(ZEN_POLICY) // should be ignored
+ .build(), false);
+
+ assertThat(zenMode.getPolicy()).isEqualTo(
+ new ZenPolicy.Builder()
+ .disallowAllSounds()
+ .hideAllVisualEffects()
+ .allowPriorityChannels(false)
+ .build());
+ }
+
+ @Test
+ public void setPolicy_setsInterruptionFilterPriority() {
+ ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .build(), false);
+
+ zenMode.setPolicy(ZEN_POLICY);
+
+ assertThat(zenMode.getRule().getInterruptionFilter()).isEqualTo(
+ INTERRUPTION_FILTER_PRIORITY);
+ assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY);
+ assertThat(zenMode.getRule().getZenPolicy()).isEqualTo(ZEN_POLICY);
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java
new file mode 100644
index 0000000..7c7972d
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.notification.modes;
+
+import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+import static android.provider.Settings.Global.ZEN_MODE_OFF;
+import static android.service.notification.Condition.SOURCE_UNKNOWN;
+import static android.service.notification.Condition.STATE_FALSE;
+import static android.service.notification.Condition.STATE_TRUE;
+import static android.service.notification.ZenPolicy.STATE_ALLOW;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AutomaticZenRule;
+import android.app.Flags;
+import android.app.NotificationManager;
+import android.app.NotificationManager.Policy;
+import android.content.Context;
+import android.net.Uri;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
+import android.service.notification.Condition;
+import android.service.notification.ZenAdapters;
+import android.service.notification.ZenDeviceEffects;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenPolicy;
+
+import com.android.settingslib.R;
+
+import com.google.common.collect.ImmutableMap;
+
+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.shadows.ShadowApplication;
+
+import java.time.Duration;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@EnableFlags(Flags.FLAG_MODES_UI)
+public class ZenModesBackendTest {
+
+ private static final String ZEN_RULE_ID = "rule";
+ private static final AutomaticZenRule ZEN_RULE =
+ new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
+ .setType(AutomaticZenRule.TYPE_DRIVING)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+ .build();
+
+ private static final AutomaticZenRule MANUAL_DND_RULE =
+ new AutomaticZenRule.Builder("Do Not Disturb", Uri.EMPTY)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+ .build();
+
+ @Mock
+ private NotificationManager mNm;
+
+ private Context mContext;
+ private ZenModesBackend mBackend;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(
+ SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT);
+
+ // Helper methods to add active/inactive rule state to a config. Returns a copy.
+ private ZenModeConfig configWithManualRule(ZenModeConfig base, boolean active) {
+ ZenModeConfig out = base.copy();
+
+ if (active) {
+ out.manualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ out.manualRule.condition =
+ new Condition(out.manualRule.conditionId, "", STATE_TRUE, SOURCE_UNKNOWN);
+ } else {
+ out.manualRule.zenMode = ZEN_MODE_OFF;
+ out.manualRule.condition =
+ new Condition(out.manualRule.conditionId, "", STATE_FALSE, SOURCE_UNKNOWN);
+ }
+ return out;
+ }
+
+ private ZenModeConfig configWithRule(ZenModeConfig base, String ruleId, AutomaticZenRule rule,
+ boolean active) {
+ ZenModeConfig out = base.copy();
+
+ // Note that there are many other fields of zenRule, but here we only set the ones
+ // relevant to determining whether or not it is active.
+ ZenModeConfig.ZenRule zenRule = new ZenModeConfig.ZenRule();
+ zenRule.pkg = "package";
+ zenRule.enabled = active;
+ zenRule.snoozing = false;
+ zenRule.condition = new Condition(rule.getConditionId(), "",
+ active ? Condition.STATE_TRUE : Condition.STATE_FALSE,
+ Condition.SOURCE_USER_ACTION);
+ out.automaticRules.put(ruleId, zenRule);
+
+ return out;
+ }
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ ShadowApplication shadowApplication = ShadowApplication.getInstance();
+ shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm);
+
+ mContext = RuntimeEnvironment.application;
+ mBackend = new ZenModesBackend(mContext);
+
+ // Default catch-all case with no data. This isn't realistic, but tests below that rely
+ // on the config to get data on rules active will create those individually.
+ when(mNm.getZenModeConfig()).thenReturn(new ZenModeConfig());
+ }
+
+ @Test
+ public void getModes_containsManualDndAndZenRules() {
+ AutomaticZenRule rule2 = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
+ .setType(AutomaticZenRule.TYPE_BEDTIME)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
+ .build();
+ Policy dndPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS,
+ Policy.PRIORITY_SENDERS_CONTACTS, Policy.PRIORITY_SENDERS_CONTACTS);
+ when(mNm.getAutomaticZenRules()).thenReturn(
+ ImmutableMap.of("rule1", ZEN_RULE, "rule2", rule2));
+ ZenModeConfig config = new ZenModeConfig();
+ config.applyNotificationPolicy(dndPolicy);
+ assertThat(config.manualRule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_ALLOW);
+ when(mNm.getZenModeConfig()).thenReturn(config);
+
+ List<ZenMode> modes = mBackend.getModes();
+
+ // all modes exist, but none of them are currently active
+ ZenPolicy zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(dndPolicy);
+ assertThat(modes).containsExactly(
+ ZenMode.manualDndMode(
+ new AutomaticZenRule.Builder(
+ mContext.getString(R.string.zen_mode_settings_title),
+ Uri.EMPTY)
+ .setType(AutomaticZenRule.TYPE_OTHER)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(zenPolicy)
+ .setManualInvocationAllowed(true)
+ .build(),
+ false),
+ new ZenMode("rule2", rule2, false),
+ new ZenMode("rule1", ZEN_RULE, false))
+ .inOrder();
+ }
+
+ @Test
+ public void getMode_manualDnd_returnsMode() {
+ Policy dndPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS,
+ Policy.PRIORITY_SENDERS_CONTACTS, Policy.PRIORITY_SENDERS_CONTACTS);
+ ZenModeConfig config = new ZenModeConfig();
+ config.applyNotificationPolicy(dndPolicy);
+ when(mNm.getZenModeConfig()).thenReturn(config);
+
+ ZenMode mode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
+
+ assertThat(mode).isEqualTo(
+ ZenMode.manualDndMode(
+ new AutomaticZenRule.Builder(
+ mContext.getString(R.string.zen_mode_settings_title), Uri.EMPTY)
+ .setType(AutomaticZenRule.TYPE_OTHER)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(ZenAdapters.notificationPolicyToZenPolicy(dndPolicy))
+ .setManualInvocationAllowed(true)
+ .build(), false));
+ }
+
+ @Test
+ public void getMode_zenRule_returnsMode() {
+ when(mNm.getAutomaticZenRule(eq(ZEN_RULE_ID))).thenReturn(ZEN_RULE);
+
+ ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
+
+ assertThat(mode).isEqualTo(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+ }
+
+ @Test
+ public void getMode_missingRule_returnsNull() {
+ when(mNm.getAutomaticZenRule(any())).thenReturn(null);
+
+ ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
+
+ assertThat(mode).isNull();
+ verify(mNm).getAutomaticZenRule(eq(ZEN_RULE_ID));
+ }
+
+ @Test
+ public void getMode_manualDnd_returnsCorrectActiveState() {
+ // Set up a base config with an active rule to make sure we're looking at the correct info
+ ZenModeConfig configWithActiveRule = configWithRule(new ZenModeConfig(), ZEN_RULE_ID,
+ ZEN_RULE, true);
+
+ // Equivalent to disallowAllSounds()
+ Policy dndPolicy = new Policy(0, 0, 0);
+ configWithActiveRule.applyNotificationPolicy(dndPolicy);
+ when(mNm.getZenModeConfig()).thenReturn(configWithActiveRule);
+
+ ZenMode mode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
+
+ // By default, manual rule is inactive
+ assertThat(mode.isActive()).isFalse();
+
+ // Now the returned config will represent the manual rule being active
+ when(mNm.getZenModeConfig()).thenReturn(configWithManualRule(configWithActiveRule, true));
+ ZenMode activeMode = mBackend.getMode(ZenMode.MANUAL_DND_MODE_ID);
+ assertThat(activeMode.isActive()).isTrue();
+ }
+
+ @Test
+ public void getMode_zenRule_returnsCorrectActiveState() {
+ // Set up a base config that has an active manual rule and "rule2", to make sure we're
+ // looking at the correct rule's info.
+ ZenModeConfig configWithActiveRules = configWithRule(
+ configWithManualRule(new ZenModeConfig(), true), // active manual rule
+ "rule2", ZEN_RULE, true); // active rule 2
+
+ when(mNm.getAutomaticZenRule(eq(ZEN_RULE_ID))).thenReturn(ZEN_RULE);
+ when(mNm.getZenModeConfig()).thenReturn(
+ configWithRule(configWithActiveRules, ZEN_RULE_ID, ZEN_RULE, false));
+
+ // Round 1: the current config should indicate that the rule is not active
+ ZenMode mode = mBackend.getMode(ZEN_RULE_ID);
+ assertThat(mode.isActive()).isFalse();
+
+ when(mNm.getZenModeConfig()).thenReturn(
+ configWithRule(configWithActiveRules, ZEN_RULE_ID, ZEN_RULE, true));
+ ZenMode activeMode = mBackend.getMode(ZEN_RULE_ID);
+ assertThat(activeMode.isActive()).isTrue();
+ }
+
+ @Test
+ public void updateMode_manualDnd_setsDeviceEffects() throws Exception {
+ ZenMode manualDnd = ZenMode.manualDndMode(
+ new AutomaticZenRule.Builder("DND", Uri.EMPTY)
+ .setZenPolicy(new ZenPolicy())
+ .setDeviceEffects(new ZenDeviceEffects.Builder()
+ .setShouldDimWallpaper(true)
+ .build())
+ .build(), false);
+
+ mBackend.updateMode(manualDnd);
+
+ verify(mNm).setManualZenRuleDeviceEffects(new ZenDeviceEffects.Builder()
+ .setShouldDimWallpaper(true)
+ .build());
+ }
+
+ @Test
+ public void updateMode_manualDnd_setsNotificationPolicy() {
+ ZenMode manualDnd = ZenMode.manualDndMode(
+ new AutomaticZenRule.Builder("DND", Uri.EMPTY)
+ .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
+ .build(), false);
+
+ mBackend.updateMode(manualDnd);
+
+ verify(mNm).setNotificationPolicy(eq(new ZenModeConfig().toNotificationPolicy(
+ new ZenPolicy.Builder().allowAllSounds().build())), eq(true));
+ }
+
+ @Test
+ public void updateMode_zenRule_updatesRule() {
+ ZenMode ruleMode = new ZenMode("rule", ZEN_RULE, false);
+
+ mBackend.updateMode(ruleMode);
+
+ verify(mNm).updateAutomaticZenRule(eq("rule"), eq(ZEN_RULE), eq(true));
+ }
+
+ @Test
+ public void activateMode_manualDnd_setsZenModeImportant() {
+ mBackend.activateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false), null);
+
+ verify(mNm).setZenMode(eq(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null),
+ any(), eq(true));
+ }
+
+ @Test
+ public void activateMode_manualDndWithDuration_setsZenModeImportantWithCondition() {
+ mBackend.activateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false),
+ Duration.ofMinutes(30));
+
+ verify(mNm).setZenMode(eq(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS),
+ eq(ZenModeConfig.toTimeCondition(mContext, 30, 0, true).id),
+ any(),
+ eq(true));
+ }
+
+ @Test
+ public void activateMode_zenRule_setsRuleStateActive() {
+ mBackend.activateMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false), null);
+
+ verify(mNm).setAutomaticZenRuleState(eq(ZEN_RULE_ID),
+ eq(new Condition(ZEN_RULE.getConditionId(), "", Condition.STATE_TRUE,
+ Condition.SOURCE_USER_ACTION)));
+ }
+
+ @Test
+ public void activateMode_zenRuleWithDuration_fails() {
+ assertThrows(IllegalArgumentException.class,
+ () -> mBackend.activateMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false),
+ Duration.ofMinutes(30)));
+ }
+
+ @Test
+ public void deactivateMode_manualDnd_setsZenModeOff() {
+ mBackend.deactivateMode(ZenMode.manualDndMode(MANUAL_DND_RULE, true));
+
+ verify(mNm).setZenMode(eq(ZEN_MODE_OFF), eq(null), any(), eq(true));
+ }
+
+ @Test
+ public void deactivateMode_zenRule_setsRuleStateInactive() {
+ mBackend.deactivateMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+
+ verify(mNm).setAutomaticZenRuleState(eq(ZEN_RULE_ID),
+ eq(new Condition(ZEN_RULE.getConditionId(), "", Condition.STATE_FALSE,
+ Condition.SOURCE_USER_ACTION)));
+ }
+
+ @Test
+ public void removeMode_zenRule_deletesRule() {
+ mBackend.removeMode(new ZenMode(ZEN_RULE_ID, ZEN_RULE, false));
+
+ verify(mNm).removeAutomaticZenRule(ZEN_RULE_ID, true);
+ }
+
+ @Test
+ public void removeMode_manualDnd_fails() {
+ assertThrows(IllegalArgumentException.class,
+ () -> mBackend.removeMode(ZenMode.manualDndMode(MANUAL_DND_RULE, false)));
+ }
+}
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index deab818..16dd4e5 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -81,7 +81,7 @@
]
}
],
-
+
"postsubmit": [
{
// Permission indicators
@@ -93,7 +93,7 @@
]
}
],
-
+
// v2/sysui/suite/test-mapping-sysui-screenshot-test
"sysui-screenshot-test": [
{
@@ -131,7 +131,7 @@
]
}
],
-
+
// v2/sysui/suite/test-mapping-sysui-screenshot-test-staged
"sysui-screenshot-test-staged": [
{
@@ -156,5 +156,13 @@
}
]
}
+ ],
+ "sysui-robo-test": [
+ {
+ "name": "SystemUIGoogleRoboRNGTests"
+ },
+ {
+ "name": "SystemUIGoogleRobo2RNGTests"
+ }
]
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 7062489..9ea435e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -49,6 +49,7 @@
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -129,7 +130,6 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.times
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Popup
@@ -994,7 +994,8 @@
shape = RoundedCornerShape(68.dp, 34.dp, 68.dp, 34.dp)
) {
Column(
- modifier = Modifier.fillMaxSize().padding(vertical = 38.dp, horizontal = 70.dp),
+ modifier = Modifier.fillMaxSize().padding(vertical = 32.dp, horizontal = 50.dp),
+ verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
@@ -1005,41 +1006,43 @@
Spacer(modifier = Modifier.size(6.dp))
Text(
text = stringResource(R.string.cta_label_to_edit_widget),
- style = MaterialTheme.typography.titleMedium,
- textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleLarge,
+ fontSize = nonScalableTextSize(22.dp),
+ lineHeight = nonScalableTextSize(28.dp),
)
- Spacer(modifier = Modifier.size(20.dp))
+ Spacer(modifier = Modifier.size(16.dp))
Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.Center,
+ modifier = Modifier.fillMaxWidth().height(56.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),
) {
OutlinedButton(
+ modifier = Modifier.fillMaxHeight(),
colors =
ButtonDefaults.buttonColors(
contentColor = colors.onPrimary,
),
border = BorderStroke(width = 1.0.dp, color = colors.primaryContainer),
- contentPadding = Dimensions.ButtonPadding,
+ contentPadding = PaddingValues(26.dp, 8.dp),
onClick = viewModel::onDismissCtaTile,
) {
Text(
text = stringResource(R.string.cta_tile_button_to_dismiss),
- fontSize = 12.sp,
+ fontSize = nonScalableTextSize(14.dp),
)
}
- Spacer(modifier = Modifier.size(14.dp))
Button(
+ modifier = Modifier.fillMaxHeight(),
colors =
ButtonDefaults.buttonColors(
containerColor = colors.primaryContainer,
contentColor = colors.onPrimaryContainer,
),
- contentPadding = Dimensions.ButtonPadding,
+ contentPadding = PaddingValues(26.dp, 8.dp),
onClick = viewModel::onOpenWidgetEditor
) {
Text(
text = stringResource(R.string.cta_tile_button_to_open_widget_editor),
- fontSize = 12.sp,
+ fontSize = nonScalableTextSize(14.dp),
)
}
}
@@ -1352,6 +1355,13 @@
}
/**
+ * Text size converted from dp value to the equivalent sp value using the current screen density,
+ * ensuring it does not scale with the font size setting.
+ */
+@Composable
+private fun nonScalableTextSize(sizeInDp: Dp) = with(LocalDensity.current) { sizeInDp.toSp() }
+
+/**
* Returns the `contentPadding` of the grid. Use the vertical padding to push the grid content area
* below the toolbar and let the grid take the max size. This ensures the item can be dragged
* outside the grid over the toolbar, without part of it getting clipped by the container.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
index 4914aea..c066ae5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
@@ -154,6 +154,7 @@
viewModel = viewModel.tileGridViewModel,
modifier =
Modifier.fillMaxWidth().heightIn(max = QuickSettingsShade.Dimensions.GridMaxHeight),
+ viewModel.editModeViewModel::startEditing,
)
Button(
onClick = { viewModel.editModeViewModel.startEditing() },
@@ -168,7 +169,7 @@
object Dimensions {
val Padding = 16.dp
val BrightnessSliderHeight = 64.dp
- val GridMaxHeight = 400.dp
+ val GridMaxHeight = 800.dp
}
object Transitions {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index dbf6cd3..e433d32 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -50,30 +50,22 @@
from(Scenes.Gone, to = Scenes.NotificationsShade, key = OpenBottomShade) {
goneToNotificationsShadeTransition(Edge.Bottom)
}
- from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() }
- from(
- Scenes.Gone,
- to = Scenes.Shade,
- key = ToSplitShade,
- ) {
- goneToSplitShadeTransition()
+ from(Scenes.Gone, to = Scenes.QuickSettingsShade) {
+ goneToQuickSettingsShadeTransition(Edge.Top)
}
- from(
- Scenes.Gone,
- to = Scenes.Shade,
- key = SlightlyFasterShadeCollapse,
- ) {
+ from(Scenes.Gone, to = Scenes.QuickSettingsShade, key = OpenBottomShade) {
+ goneToQuickSettingsShadeTransition(Edge.Bottom)
+ }
+ from(Scenes.Gone, to = Scenes.Shade) { goneToShadeTransition() }
+ from(Scenes.Gone, to = Scenes.Shade, key = ToSplitShade) { goneToSplitShadeTransition() }
+ from(Scenes.Gone, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) {
goneToShadeTransition(durationScale = 0.9)
}
from(Scenes.Gone, to = Scenes.QuickSettings) { goneToQuickSettingsTransition() }
- from(
- Scenes.Gone,
- to = Scenes.QuickSettings,
- key = SlightlyFasterShadeCollapse,
- ) {
+ from(Scenes.Gone, to = Scenes.QuickSettings, key = SlightlyFasterShadeCollapse) {
goneToQuickSettingsTransition(durationScale = 0.9)
}
- from(Scenes.Gone, to = Scenes.QuickSettingsShade) { goneToQuickSettingsShadeTransition() }
+
from(Scenes.Lockscreen, to = Scenes.Bouncer) { lockscreenToBouncerTransition() }
from(Scenes.Lockscreen, to = Scenes.Communal) { lockscreenToCommunalTransition() }
from(Scenes.Lockscreen, to = Scenes.NotificationsShade) {
@@ -83,18 +75,10 @@
lockscreenToQuickSettingsShadeTransition()
}
from(Scenes.Lockscreen, to = Scenes.Shade) { lockscreenToShadeTransition() }
- from(
- Scenes.Lockscreen,
- to = Scenes.Shade,
- key = ToSplitShade,
- ) {
+ from(Scenes.Lockscreen, to = Scenes.Shade, key = ToSplitShade) {
lockscreenToSplitShadeTransition()
}
- from(
- Scenes.Lockscreen,
- to = Scenes.Shade,
- key = SlightlyFasterShadeCollapse,
- ) {
+ from(Scenes.Lockscreen, to = Scenes.Shade, key = SlightlyFasterShadeCollapse) {
lockscreenToShadeTransition(durationScale = 0.9)
}
from(Scenes.Lockscreen, to = Scenes.QuickSettings) { lockscreenToQuickSettingsTransition() }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt
index 225ca4e..8a03e29 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToQuickSettingsShadeTransition.kt
@@ -16,10 +16,12 @@
package com.android.systemui.scene.ui.composable.transitions
+import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
fun TransitionBuilder.goneToQuickSettingsShadeTransition(
+ edge: Edge = Edge.Top,
durationScale: Double = 1.0,
) {
- toQuickSettingsShadeTransition(durationScale)
+ toQuickSettingsShadeTransition(edge, durationScale)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt
index ce24f5e..19aa3a7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToQuickSettingsShadeTransition.kt
@@ -16,10 +16,11 @@
package com.android.systemui.scene.ui.composable.transitions
+import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
fun TransitionBuilder.lockscreenToQuickSettingsShadeTransition(
durationScale: Double = 1.0,
) {
- toQuickSettingsShadeTransition(durationScale)
+ toQuickSettingsShadeTransition(Edge.Top, durationScale)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
index ec2f14f..9d13647 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToQuickSettingsShadeTransition.kt
@@ -19,17 +19,15 @@
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.unit.IntSize
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.TransitionBuilder
import com.android.compose.animation.scene.UserActionDistance
-import com.android.compose.animation.scene.UserActionDistanceScope
import com.android.systemui.shade.ui.composable.OverlayShade
import com.android.systemui.shade.ui.composable.Shade
import kotlin.time.Duration.Companion.milliseconds
fun TransitionBuilder.toQuickSettingsShadeTransition(
+ edge: Edge = Edge.Top,
durationScale: Double = 1.0,
) {
spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
@@ -38,17 +36,9 @@
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
)
- distance =
- object : UserActionDistance {
- override fun UserActionDistanceScope.absoluteDistance(
- fromSceneSize: IntSize,
- orientation: Orientation,
- ): Float {
- return fromSceneSize.height.toFloat() * 2 / 3f
- }
- }
+ distance = UserActionDistance { fromSceneSize, _ -> fromSceneSize.height.toFloat() * 2 / 3f }
- translate(OverlayShade.Elements.Panel, Edge.Top)
+ translate(OverlayShade.Elements.Panel, edge)
fractionRange(end = .5f) { fade(OverlayShade.Elements.Scrim) }
}
diff --git a/packages/SystemUI/lint-baseline.xml b/packages/SystemUI/lint-baseline.xml
index 4def93f..2fd7f1b 100644
--- a/packages/SystemUI/lint-baseline.xml
+++ b/packages/SystemUI/lint-baseline.xml
@@ -27088,17 +27088,6 @@
<issue
id="UselessParent"
- message="This `FrameLayout` layout or its `LinearLayout` parent is unnecessary"
- errorLine1=" <FrameLayout"
- errorLine2=" ~~~~~~~~~~~">
- <location
- file="frameworks/base/packages/SystemUI/res/layout/media_output_list_item.xml"
- line="24"
- column="6"/>
- </issue>
-
- <issue
- id="UselessParent"
message="This `LinearLayout` layout or its `FrameLayout` parent is possibly unnecessary; transfer the `background` attribute to the other view"
errorLine1=" <LinearLayout"
errorLine2=" ~~~~~~~~~~~~">
@@ -30587,17 +30576,6 @@
<issue
id="ContentDescription"
message="Missing `contentDescription` attribute on image"
- errorLine1=" <ImageView"
- errorLine2=" ~~~~~~~~~">
- <location
- file="frameworks/base/packages/SystemUI/res/layout/media_output_list_item.xml"
- line="54"
- column="14"/>
- </issue>
-
- <issue
- id="ContentDescription"
- message="Missing `contentDescription` attribute on image"
errorLine1=" <ImageView"
errorLine2=" ~~~~~~~~~">
<location
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryTest.kt
new file mode 100644
index 0000000..14d6094
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryTest.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.systemui.qs.panels.data.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PaginatedGridRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ val underTest = kosmos.paginatedGridRepository
+
+ @Test
+ fun rows_followsConfig() =
+ with(kosmos) {
+ testScope.runTest {
+ val rows by collectLastValue(underTest.rows)
+
+ setRowsInConfig(3)
+ assertThat(rows).isEqualTo(3)
+
+ setRowsInConfig(6)
+ assertThat(rows).isEqualTo(6)
+ }
+ }
+
+ private fun setRowsInConfig(rows: Int) =
+ with(kosmos) {
+ testCase.context.orCreateTestableResources.addOverride(
+ R.integer.quick_settings_max_rows,
+ rows,
+ )
+ fakeConfigurationRepository.onConfigurationChange()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
new file mode 100644
index 0000000..914a095
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayoutTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.systemui.qs.panels.ui.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.panels.data.repository.IconTilesRepository
+import com.android.systemui.qs.panels.data.repository.iconTilesRepository
+import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class InfiniteGridLayoutTest : SysuiTestCase() {
+ private val kosmos =
+ testKosmos().apply {
+ iconTilesRepository =
+ object : IconTilesRepository {
+ override fun isIconTile(spec: TileSpec): Boolean {
+ return spec.spec.startsWith("small")
+ }
+ }
+ }
+
+ private val underTest =
+ with(kosmos) {
+ InfiniteGridLayout(
+ iconTilesViewModel,
+ fixedColumnsSizeViewModel,
+ )
+ }
+
+ @Test
+ fun correctPagination_underOnePage_sameOrder() =
+ with(kosmos) {
+ testScope.runTest {
+ val rows = 3
+ val columns = 4
+
+ val tiles =
+ listOf(
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ largeTile(),
+ smallTile()
+ )
+
+ val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
+
+ assertThat(pages).hasSize(1)
+ assertThat(pages[0]).isEqualTo(tiles)
+ }
+ }
+
+ @Test
+ fun correctPagination_twoPages_sameOrder() =
+ with(kosmos) {
+ testScope.runTest {
+ val rows = 3
+ val columns = 4
+
+ val tiles =
+ listOf(
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ )
+ // --- Page 1 ---
+ // [L L] [S] [S]
+ // [L L] [L L]
+ // [S] [S] [L L]
+ // --- Page 2 ---
+ // [L L] [S] [S]
+ // [L L]
+
+ val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
+
+ assertThat(pages).hasSize(2)
+ assertThat(pages[0]).isEqualTo(tiles.take(8))
+ assertThat(pages[1]).isEqualTo(tiles.drop(8))
+ }
+ }
+
+ companion object {
+ fun largeTile() = MockTileViewModel(TileSpec.create("large"))
+
+ fun smallTile() = MockTileViewModel(TileSpec.create("small"))
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PaginatableGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PaginatableGridLayoutTest.kt
new file mode 100644
index 0000000..6df3f8d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PaginatableGridLayoutTest.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.systemui.qs.panels.ui.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PaginatableGridLayoutTest : SysuiTestCase() {
+ @Test
+ fun correctRows_gapsAtEnd() {
+ val columns = 6
+
+ val sizedTiles =
+ listOf(
+ largeTile(),
+ extraLargeTile(),
+ largeTile(),
+ smallTile(),
+ largeTile(),
+ )
+
+ // [L L] [XL XL XL]
+ // [L L] [S] [L L]
+
+ val rows = PaginatableGridLayout.splitInRows(sizedTiles, columns)
+
+ assertThat(rows).hasSize(2)
+ assertThat(rows[0]).isEqualTo(sizedTiles.take(2))
+ assertThat(rows[1]).isEqualTo(sizedTiles.drop(2))
+ }
+
+ @Test
+ fun correctRows_fullLastRow_noEmptyRow() {
+ val columns = 6
+
+ val sizedTiles =
+ listOf(
+ largeTile(),
+ extraLargeTile(),
+ smallTile(),
+ )
+
+ // [L L] [XL XL XL] [S]
+
+ val rows = PaginatableGridLayout.splitInRows(sizedTiles, columns)
+
+ assertThat(rows).hasSize(1)
+ assertThat(rows[0]).isEqualTo(sizedTiles)
+ }
+
+ companion object {
+ fun extraLargeTile() = SizedTile(MockTileViewModel(TileSpec.create("XLarge")), 3)
+
+ fun largeTile() = SizedTile(MockTileViewModel(TileSpec.create("large")), 2)
+
+ fun smallTile() = SizedTile(MockTileViewModel(TileSpec.create("small")), 1)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayoutTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayoutTest.kt
new file mode 100644
index 0000000..3354b4d4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayoutTest.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.systemui.qs.panels.ui.compose
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.panels.data.repository.IconTilesRepository
+import com.android.systemui.qs.panels.data.repository.iconTilesRepository
+import com.android.systemui.qs.panels.ui.viewmodel.MockTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.partitionedGridViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PartitionedGridLayoutTest : SysuiTestCase() {
+ private val kosmos =
+ testKosmos().apply {
+ iconTilesRepository =
+ object : IconTilesRepository {
+ override fun isIconTile(spec: TileSpec): Boolean {
+ return spec.spec.startsWith("small")
+ }
+ }
+ }
+
+ private val underTest = with(kosmos) { PartitionedGridLayout(partitionedGridViewModel) }
+
+ @Test
+ fun correctPagination_underOnePage_partitioned_sameRelativeOrder() =
+ with(kosmos) {
+ testScope.runTest {
+ val rows = 3
+ val columns = 4
+
+ val tiles =
+ listOf(
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ largeTile(),
+ smallTile()
+ )
+ val (smallTiles, largeTiles) =
+ tiles.partition { iconTilesViewModel.isIconTile(it.spec) }
+
+ // [L L] [L L]
+ // [L L]
+ // [S] [S] [S]
+
+ val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
+
+ Truth.assertThat(pages).hasSize(1)
+ Truth.assertThat(pages[0]).isEqualTo(largeTiles + smallTiles)
+ }
+ }
+
+ @Test
+ fun correctPagination_twoPages_partitioned_sameRelativeOrder() =
+ with(kosmos) {
+ testScope.runTest {
+ val rows = 3
+ val columns = 4
+
+ val tiles =
+ listOf(
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ largeTile(),
+ smallTile(),
+ smallTile(),
+ )
+ // --- Page 1 ---
+ // [L L] [L L]
+ // [L L]
+ // [S] [S] [S] [S]
+ // --- Page 2 ---
+ // [S] [S]
+
+ val (smallTiles, largeTiles) =
+ tiles.partition { iconTilesViewModel.isIconTile(it.spec) }
+
+ val pages = underTest.splitIntoPages(tiles, rows = rows, columns = columns)
+
+ val expectedPage0 = largeTiles + smallTiles.take(4)
+ val expectedPage1 = smallTiles.drop(4)
+
+ Truth.assertThat(pages).hasSize(2)
+ Truth.assertThat(pages[0]).isEqualTo(expectedPage0)
+ Truth.assertThat(pages[1]).isEqualTo(expectedPage1)
+ }
+ }
+
+ companion object {
+ fun largeTile() = MockTileViewModel(TileSpec.create("large"))
+
+ fun smallTile() = MockTileViewModel(TileSpec.create("small"))
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractorTest.kt
new file mode 100644
index 0000000..2194c75
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractorTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.systemui.qs.tiles.impl.irecording
+
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.recordissue.IssueRecordingState
+import com.android.systemui.settings.fakeUserFileManager
+import com.android.systemui.settings.userTracker
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IssueRecordingDataInteractorTest : SysuiTestCase() {
+
+ private val kosmos = Kosmos().also { it.testCase = this }
+ private val userTracker = kosmos.userTracker
+ private val userFileManager = kosmos.fakeUserFileManager
+ private val testUser = UserHandle.of(1)
+
+ lateinit var state: IssueRecordingState
+ private lateinit var underTest: IssueRecordingDataInteractor
+
+ @Before
+ fun setup() {
+ state = IssueRecordingState(userTracker, userFileManager)
+ underTest = IssueRecordingDataInteractor(state, kosmos.testScope.testScheduler)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun emitsEvent_whenIsRecordingStatusChanges_correctly() {
+ kosmos.testScope.runTest {
+ val data by
+ collectLastValue(
+ underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest))
+ )
+ runCurrent()
+ Truth.assertThat(data?.isRecording).isFalse()
+
+ state.isRecording = true
+ runCurrent()
+ Truth.assertThat(data?.isRecording).isTrue()
+
+ state.isRecording = false
+ runCurrent()
+ Truth.assertThat(data?.isRecording).isFalse()
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapperTest.kt
new file mode 100644
index 0000000..2444229
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapperTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.systemui.qs.tiles.impl.irecording
+
+import android.content.res.mainResources
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
+import com.android.systemui.recordissue.RecordIssueModule
+import com.android.systemui.res.R
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IssueRecordingMapperTest : SysuiTestCase() {
+ private val kosmos = Kosmos().also { it.testCase = this }
+ private val uiConfig =
+ QSTileUIConfig.Resource(R.drawable.qs_record_issue_icon_off, R.string.qs_record_issue_label)
+ private val config =
+ QSTileConfig(
+ TileSpec.create(RecordIssueModule.TILE_SPEC),
+ uiConfig,
+ kosmos.qsEventLogger.getNewInstanceId()
+ )
+ private val resources = kosmos.mainResources
+ private val theme = resources.newTheme()
+
+ @Test
+ fun whenData_isRecording_useCorrectResources() {
+ val underTest = IssueRecordingMapper(resources, theme)
+ val tileState = underTest.map(config, IssueRecordingModel(true))
+ Truth.assertThat(tileState.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE)
+ }
+
+ @Test
+ fun whenData_isNotRecording_useCorrectResources() {
+ val underTest = IssueRecordingMapper(resources, theme)
+ val tileState = underTest.map(config, IssueRecordingModel(false))
+ Truth.assertThat(tileState.activationState).isEqualTo(QSTileState.ActivationState.INACTIVE)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt
new file mode 100644
index 0000000..4e58069
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractorTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.systemui.qs.tiles.impl.irecording
+
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.dialogTransitionAnimator
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.qs.pipeline.domain.interactor.panelInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.recordissue.RecordIssueDialogDelegate
+import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.settings.userTracker
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil
+import com.android.systemui.statusbar.policy.keyguardStateController
+import com.google.common.truth.Truth
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IssueRecordingUserActionInteractorTest : SysuiTestCase() {
+
+ val user = UserHandle(1)
+ val kosmos = Kosmos().also { it.testCase = this }
+
+ private lateinit var userContextProvider: UserContextProvider
+ private lateinit var underTest: IssueRecordingUserActionInteractor
+
+ private var hasCreatedDialogDelegate: Boolean = false
+
+ @Before
+ fun setup() {
+ hasCreatedDialogDelegate = false
+ with(kosmos) {
+ val factory =
+ object : RecordIssueDialogDelegate.Factory {
+ override fun create(onStarted: Runnable): RecordIssueDialogDelegate {
+ hasCreatedDialogDelegate = true
+
+ // Inside some tests in presubmit, createDialog throws an error because
+ // the test thread's looper hasn't been prepared, and Dialog.class
+ // internally is creating a new handler. For testing, we only care that the
+ // dialog is created, so using a mock is acceptable here.
+ return mock(RecordIssueDialogDelegate::class.java)
+ }
+ }
+
+ userContextProvider = userTracker
+ underTest =
+ IssueRecordingUserActionInteractor(
+ testDispatcher,
+ KeyguardDismissUtil(
+ keyguardStateController,
+ statusBarStateController,
+ activityStarter
+ ),
+ keyguardStateController,
+ dialogTransitionAnimator,
+ panelInteractor,
+ userTracker,
+ factory
+ )
+ }
+ }
+
+ @Test
+ fun handleInput_showsPromptToStartRecording_whenNotRecordingAlready() {
+ kosmos.testScope.runTest {
+ underTest.handleInput(
+ QSTileInput(user, QSTileUserAction.Click(null), IssueRecordingModel(false))
+ )
+ Truth.assertThat(hasCreatedDialogDelegate).isTrue()
+ }
+ }
+
+ @Test
+ fun handleInput_attemptsToStopRecording_whenRecording() {
+ kosmos.testScope.runTest {
+ val input = QSTileInput(user, QSTileUserAction.Click(null), IssueRecordingModel(true))
+ try {
+ underTest.handleInput(input)
+ } catch (e: NullPointerException) {
+ // As of 06/07/2024, PendingIntent.startService is not easily mockable and throws
+ // an NPE inside IActivityManager. Catching that here and ignore it, then verify
+ // mock interactions were done correctly
+ }
+ Truth.assertThat(hasCreatedDialogDelegate).isFalse()
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 68e4cda..40315a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -148,36 +148,40 @@
assertThat(important).isTrue()
}
+ // NOTE: The empty shade view and the footer view should be mutually exclusive.
+
@Test
- fun shouldIncludeEmptyShadeView_trueWhenNoNotifs() =
+ fun shouldShowEmptyShadeView_trueWhenNoNotifs() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
runCurrent()
// THEN empty shade is visible
- assertThat(shouldInclude).isTrue()
+ assertThat(shouldShowEmptyShadeView).isTrue()
+ assertThat(shouldIncludeFooterView?.value).isFalse()
}
@Test
- fun shouldIncludeEmptyShadeView_falseWhenNotifs() =
+ fun shouldShowEmptyShadeView_falseWhenNotifs() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
runCurrent()
// THEN empty shade is not visible
- assertThat(shouldInclude).isFalse()
+ assertThat(shouldShowEmptyShadeView).isFalse()
}
@Test
- fun shouldIncludeEmptyShadeView_falseWhenQsExpandedDefault() =
+ fun shouldShowEmptyShadeView_falseWhenQsExpandedDefault() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -186,13 +190,14 @@
runCurrent()
// THEN empty shade is not visible
- assertThat(shouldInclude).isFalse()
+ assertThat(shouldShow).isFalse()
}
@Test
- fun shouldIncludeEmptyShadeView_trueWhenQsExpandedInSplitShade() =
+ fun shouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -205,13 +210,15 @@
runCurrent()
// THEN empty shade is visible
- assertThat(shouldInclude).isTrue()
+ assertThat(shouldShowEmptyShadeView).isTrue()
+ assertThat(shouldIncludeFooterView?.value).isFalse()
}
@Test
- fun shouldIncludeEmptyShadeView_trueWhenLockedShade() =
+ fun shouldShowEmptyShadeView_trueWhenLockedShade() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -220,13 +227,14 @@
runCurrent()
// THEN empty shade is visible
- assertThat(shouldInclude).isTrue()
+ assertThat(shouldShowEmptyShadeView).isTrue()
+ assertThat(shouldIncludeFooterView?.value).isFalse()
}
@Test
- fun shouldIncludeEmptyShadeView_falseWhenKeyguard() =
+ fun shouldShowEmptyShadeView_falseWhenKeyguard() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -235,13 +243,13 @@
runCurrent()
// THEN empty shade is not visible
- assertThat(shouldInclude).isFalse()
+ assertThat(shouldShow).isFalse()
}
@Test
- fun shouldIncludeEmptyShadeView_falseWhenStartingToSleep() =
+ fun shouldShowEmptyShadeView_falseWhenStartingToSleep() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView)
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
@@ -252,7 +260,7 @@
runCurrent()
// THEN empty shade is not visible
- assertThat(shouldInclude).isFalse()
+ assertThat(shouldShow).isFalse()
}
@Test
@@ -308,7 +316,8 @@
@Test
fun shouldIncludeFooterView_trueWhenShade() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -318,13 +327,15 @@
runCurrent()
// THEN footer is visible
- assertThat(shouldInclude?.value).isTrue()
+ assertThat(shouldIncludeFooterView?.value).isTrue()
+ assertThat(shouldShowEmptyShadeView).isFalse()
}
@Test
fun shouldIncludeFooterView_trueWhenLockedShade() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -334,7 +345,8 @@
runCurrent()
// THEN footer is visible
- assertThat(shouldInclude?.value).isTrue()
+ assertThat(shouldIncludeFooterView?.value).isTrue()
+ assertThat(shouldShowEmptyShadeView).isFalse()
}
@Test
@@ -410,7 +422,8 @@
@Test
fun shouldIncludeFooterView_trueWhenQsExpandedSplitShade() =
testScope.runTest {
- val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldIncludeFooterView by collectLastValue(underTest.shouldIncludeFooterView)
+ val shouldShowEmptyShadeView by collectLastValue(underTest.shouldShowEmptyShadeView)
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
@@ -425,7 +438,8 @@
runCurrent()
// THEN footer is visible
- assertThat(shouldInclude?.value).isTrue()
+ assertThat(shouldIncludeFooterView?.value).isTrue()
+ assertThat(shouldShowEmptyShadeView).isFalse()
}
@Test
diff --git a/packages/SystemUI/res/drawable/ic_check_box.xml b/packages/SystemUI/res/drawable/ic_check_box.xml
deleted file mode 100644
index a8d1a65..0000000
--- a/packages/SystemUI/res/drawable/ic_check_box.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<!--
- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License
- -->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item
- android:id="@+id/checked"
- android:state_checked="true"
- android:drawable="@drawable/ic_check_box_blue_24dp" />
- <item
- android:id="@+id/unchecked"
- android:state_checked="false"
- android:drawable="@drawable/ic_check_box_outline_24dp" />
-</selector>
diff --git a/packages/SystemUI/res/drawable/ic_check_box_blue_24dp.xml b/packages/SystemUI/res/drawable/ic_check_box_blue_24dp.xml
deleted file mode 100644
index 43cae69..0000000
--- a/packages/SystemUI/res/drawable/ic_check_box_blue_24dp.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<!--
- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License
- -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24"
- android:viewportHeight="24">
- <path
- android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"
- android:fillColor="#4285F4"/>
-</vector>
-
diff --git a/packages/SystemUI/res/drawable/ic_check_box_outline_24dp.xml b/packages/SystemUI/res/drawable/ic_check_box_outline_24dp.xml
deleted file mode 100644
index f6f453a..0000000
--- a/packages/SystemUI/res/drawable/ic_check_box_outline_24dp.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<!--
- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License
- -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24"
- android:viewportHeight="24">
- <path
- android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"
- android:fillColor="#757575"/>
-</vector>
-
diff --git a/packages/SystemUI/res/drawable/ic_speaker_group_black_24dp.xml b/packages/SystemUI/res/drawable/ic_speaker_group_black_24dp.xml
deleted file mode 100644
index ae0d562..0000000
--- a/packages/SystemUI/res/drawable/ic_speaker_group_black_24dp.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<!--
- Copyright (C) 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License
- -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24"
- android:viewportHeight="24">
- <path
- android:pathData="M18.2,1L9.8,1C8.81,1 8,1.81 8,2.8v14.4c0,0.99 0.81,1.79 1.8,1.79l8.4,0.01c0.99,0 1.8,-0.81 1.8,-1.8L20,2.8c0,-0.99 -0.81,-1.8 -1.8,-1.8zM14,3c1.1,0 2,0.89 2,2s-0.9,2 -2,2 -2,-0.89 -2,-2 0.9,-2 2,-2zM14,16.5c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z"
- android:fillColor="#000000"/>
- <path
- android:pathData="M14,12.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
- android:fillColor="#000000"/>
- <path
- android:pathData="M6,5H4v16c0,1.1 0.89,2 2,2h10v-2H6V5z"
- android:fillColor="#000000"/>
-</vector>
diff --git a/packages/SystemUI/res/layout/media_output_list_item.xml b/packages/SystemUI/res/layout/media_output_list_item.xml
deleted file mode 100644
index 5b1ec7f..0000000
--- a/packages/SystemUI/res/layout/media_output_list_item.xml
+++ /dev/null
@@ -1,146 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2020 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/device_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical">
- <FrameLayout
- android:layout_width="match_parent"
- android:layout_height="64dp"
- android:layout_marginStart="16dp"
- android:layout_marginEnd="16dp"
- android:layout_marginBottom="12dp">
- <FrameLayout
- android:id="@+id/item_layout"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@drawable/media_output_item_background"
- android:layout_gravity="center_vertical|start">
- <com.android.systemui.media.dialog.MediaOutputSeekbar
- android:id="@+id/volume_seekbar"
- android:splitTrack="false"
- android:visibility="gone"
- android:paddingStart="0dp"
- android:paddingEnd="0dp"
- android:background="@null"
- android:contentDescription="@string/media_output_dialog_accessibility_seekbar"
- android:progressDrawable="@drawable/media_output_dialog_seekbar_background"
- android:thumb="@null"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
- </FrameLayout>
-
- <FrameLayout
- android:layout_width="56dp"
- android:layout_height="64dp"
- android:layout_gravity="center_vertical|start">
- <ImageView
- android:id="@+id/title_icon"
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:layout_gravity="center"/>
- </FrameLayout>
-
- <TextView
- android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_vertical|start"
- android:layout_marginStart="56dp"
- android:layout_marginEnd="56dp"
- android:ellipsize="end"
- android:maxLines="1"
- android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
- android:textSize="16sp"/>
-
- <LinearLayout
- android:id="@+id/two_line_layout"
- android:orientation="vertical"
- android:layout_width="wrap_content"
- android:layout_gravity="center_vertical|start"
- android:layout_height="48dp"
- android:layout_marginEnd="56dp"
- android:layout_marginStart="56dp">
- <TextView
- android:id="@+id/two_line_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:maxLines="1"
- android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
- android:textColor="@color/media_dialog_item_main_content"
- android:textSize="16sp"/>
- <TextView
- android:id="@+id/subtitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:maxLines="1"
- android:textColor="@color/media_dialog_item_main_content"
- android:textSize="14sp"
- android:fontFamily="@*android:string/config_bodyFontFamily"
- android:visibility="gone"/>
- </LinearLayout>
-
- <ProgressBar
- android:id="@+id/volume_indeterminate_progress"
- style="?android:attr/progressBarStyleSmallTitle"
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:layout_marginEnd="16dp"
- android:indeterminate="true"
- android:layout_gravity="end|center"
- android:indeterminateOnly="true"
- android:visibility="gone"/>
-
- <ImageView
- android:id="@+id/media_output_item_status"
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:layout_marginEnd="16dp"
- android:indeterminate="true"
- android:layout_gravity="end|center"
- android:indeterminateOnly="true"
- android:importantForAccessibility="no"
- android:visibility="gone"/>
-
- <LinearLayout
- android:id="@+id/end_action_area"
- android:visibility="gone"
- android:orientation="vertical"
- android:layout_width="48dp"
- android:layout_height="64dp"
- android:layout_gravity="end|center"
- android:gravity="center_vertical">
- <CheckBox
- android:id="@+id/check_box"
- android:focusable="false"
- android:importantForAccessibility="no"
- android:layout_width="24dp"
- android:layout_height="24dp"
- android:layout_marginEnd="16dp"
- android:layout_gravity="end"
- android:button="@drawable/ic_circle_check_box"
- android:visibility="gone"
- />
-
- </LinearLayout>
- </FrameLayout>
-</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PaginatedBaseLayoutType.kt
similarity index 76%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
copy to packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PaginatedBaseLayoutType.kt
index d8af3fa..0285cbd 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PaginatedBaseLayoutType.kt
@@ -14,8 +14,11 @@
* limitations under the License.
*/
-package com.android.systemui.qs.panels.data.repository
+package com.android.systemui.qs.panels.dagger
-import com.android.systemui.kosmos.Kosmos
+import javax.inject.Qualifier
-val Kosmos.infiniteGridSizeRepository by Kosmos.Fixture { InfiniteGridSizeRepository() }
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class PaginatedBaseLayoutType
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
index 7b67993..c214361 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
@@ -30,18 +30,21 @@
import com.android.systemui.qs.panels.shared.model.GridLayoutType
import com.android.systemui.qs.panels.shared.model.IconLabelVisibilityLog
import com.android.systemui.qs.panels.shared.model.InfiniteGridLayoutType
+import com.android.systemui.qs.panels.shared.model.PaginatedGridLayoutType
import com.android.systemui.qs.panels.shared.model.PartitionedGridLayoutType
import com.android.systemui.qs.panels.shared.model.StretchedGridLayoutType
import com.android.systemui.qs.panels.ui.compose.GridLayout
import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
+import com.android.systemui.qs.panels.ui.compose.PaginatableGridLayout
+import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout
import com.android.systemui.qs.panels.ui.compose.PartitionedGridLayout
import com.android.systemui.qs.panels.ui.compose.StretchedGridLayout
+import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModelImpl
import com.android.systemui.qs.panels.ui.viewmodel.IconLabelVisibilityViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconLabelVisibilityViewModelImpl
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModelImpl
-import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModelImpl
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -62,14 +65,24 @@
@Binds fun bindIconTilesViewModel(impl: IconTilesViewModelImpl): IconTilesViewModel
- @Binds fun bindGridSizeViewModel(impl: InfiniteGridSizeViewModelImpl): InfiniteGridSizeViewModel
+ @Binds fun bindGridSizeViewModel(impl: FixedColumnsSizeViewModelImpl): FixedColumnsSizeViewModel
@Binds
fun bindIconLabelVisibilityViewModel(
impl: IconLabelVisibilityViewModelImpl
): IconLabelVisibilityViewModel
- @Binds @Named("Default") fun bindDefaultGridLayout(impl: PartitionedGridLayout): GridLayout
+ @Binds
+ @PaginatedBaseLayoutType
+ fun bindPaginatedBaseGridLayout(impl: PartitionedGridLayout): PaginatableGridLayout
+
+ @Binds
+ @PaginatedBaseLayoutType
+ fun bindPaginatedBaseConsistencyInteractor(
+ impl: NoopGridConsistencyInteractor
+ ): GridTypeConsistencyInteractor
+
+ @Binds @Named("Default") fun bindDefaultGridLayout(impl: PaginatedGridLayout): GridLayout
companion object {
@Provides
@@ -109,6 +122,14 @@
}
@Provides
+ @IntoSet
+ fun providePaginatedGridLayout(
+ gridLayout: PaginatedGridLayout
+ ): Pair<GridLayoutType, GridLayout> {
+ return Pair(PaginatedGridLayoutType, gridLayout)
+ }
+
+ @Provides
fun provideGridLayoutMap(
entries: Set<@JvmSuppressWildcards Pair<GridLayoutType, GridLayout>>
): Map<GridLayoutType, GridLayout> {
@@ -147,6 +168,14 @@
}
@Provides
+ @IntoSet
+ fun providePaginatedGridConsistencyInteractor(
+ @PaginatedBaseLayoutType consistencyInteractor: GridTypeConsistencyInteractor,
+ ): Pair<GridLayoutType, GridTypeConsistencyInteractor> {
+ return Pair(PaginatedGridLayoutType, consistencyInteractor)
+ }
+
+ @Provides
fun provideGridConsistencyInteractorMap(
entries: Set<@JvmSuppressWildcards Pair<GridLayoutType, GridTypeConsistencyInteractor>>
): Map<GridLayoutType, GridTypeConsistencyInteractor> {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepository.kt
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepository.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepository.kt
index 43ccdf66..32ce973 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepository.kt
@@ -23,7 +23,7 @@
import kotlinx.coroutines.flow.asStateFlow
@SysUISingleton
-class InfiniteGridSizeRepository @Inject constructor() {
+class FixedColumnsRepository @Inject constructor() {
// Number of columns in the narrowest state for consistency
private val _columns = MutableStateFlow(4)
val columns: StateFlow<Int> = _columns.asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt
index 44d8688..47c4ffd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/GridLayoutTypeRepository.kt
@@ -18,7 +18,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.panels.shared.model.GridLayoutType
-import com.android.systemui.qs.panels.shared.model.PartitionedGridLayoutType
+import com.android.systemui.qs.panels.shared.model.PaginatedGridLayoutType
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -26,13 +26,14 @@
interface GridLayoutTypeRepository {
val layout: StateFlow<GridLayoutType>
+
fun setLayout(type: GridLayoutType)
}
@SysUISingleton
class GridLayoutTypeRepositoryImpl @Inject constructor() : GridLayoutTypeRepository {
private val _layout: MutableStateFlow<GridLayoutType> =
- MutableStateFlow(PartitionedGridLayoutType)
+ MutableStateFlow(PaginatedGridLayoutType)
override val layout = _layout.asStateFlow()
override fun setLayout(type: GridLayoutType) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepository.kt
new file mode 100644
index 0000000..26b2e2b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepository.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.systemui.qs.panels.data.repository
+
+import android.content.res.Resources
+import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.util.kotlin.emitOnStart
+import javax.inject.Inject
+import kotlinx.coroutines.flow.map
+
+/**
+ * Provides the number of [rows] to use with a paginated grid, by tracking the resource
+ * [R.integer.quick_settings_max_rows].
+ */
+@SysUISingleton
+class PaginatedGridRepository
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ configurationRepository: ConfigurationRepository,
+) {
+ val rows =
+ configurationRepository.onConfigurationChange.emitOnStart().map {
+ resources.getInteger(R.integer.quick_settings_max_rows)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractor.kt
similarity index 83%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractor.kt
index 13c6072..9591002 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractor.kt
@@ -17,11 +17,11 @@
package com.android.systemui.qs.panels.domain.interactor
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.panels.data.repository.InfiniteGridSizeRepository
+import com.android.systemui.qs.panels.data.repository.FixedColumnsRepository
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow
@SysUISingleton
-class InfiniteGridSizeInteractor @Inject constructor(repo: InfiniteGridSizeRepository) {
+class FixedColumnsSizeInteractor @Inject constructor(repo: FixedColumnsRepository) {
val columns: StateFlow<Int> = repo.columns
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
index e99c64c..0fe79af 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractor.kt
@@ -28,7 +28,7 @@
@Inject
constructor(
private val iconTilesInteractor: IconTilesInteractor,
- private val gridSizeInteractor: InfiniteGridSizeInteractor
+ private val gridSizeInteractor: FixedColumnsSizeInteractor
) : GridTypeConsistencyInteractor {
/**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopConsistencyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractor.kt
similarity index 75%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopConsistencyInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractor.kt
index 97ceacc..d7d1ce9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/NoopConsistencyInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractor.kt
@@ -17,10 +17,14 @@
package com.android.systemui.qs.panels.domain.interactor
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.panels.data.repository.PaginatedGridRepository
import javax.inject.Inject
@SysUISingleton
-class NoopConsistencyInteractor @Inject constructor() : GridTypeConsistencyInteractor {
- override fun reconcileTiles(tiles: List<TileSpec>): List<TileSpec> = tiles
+class PaginatedGridInteractor
+@Inject
+constructor(paginatedGridRepository: PaginatedGridRepository) {
+ val rows = paginatedGridRepository.rows
+
+ val defaultRows = 4
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt
index 9550ddb..b1942fe 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/GridLayoutType.kt
@@ -34,3 +34,6 @@
/** Grid type grouping large tiles on top and icon tiles at the bottom. */
data object PartitionedGridLayoutType : GridLayoutType
+
+/** Grid type for a paginated list of tiles. It will delegate to some other layout type. */
+data object PaginatedGridLayoutType : GridLayoutType
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
index 8806931..e2f6bcf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
@@ -18,15 +18,19 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.shared.model.TileRow
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
+/** A layout of tiles, indicating how they should be composed when showing in QS or in edit mode. */
interface GridLayout {
@Composable
fun TileGrid(
tiles: List<TileViewModel>,
modifier: Modifier,
+ editModeStart: () -> Unit,
)
@Composable
@@ -37,3 +41,49 @@
onRemoveTile: (TileSpec) -> Unit,
)
}
+
+/**
+ * A type of [GridLayout] that can be paginated, to use together with [PaginatedGridLayout].
+ *
+ * [splitIntoPages] determines how to split a list of tiles based on the number of rows and columns
+ * available.
+ */
+interface PaginatableGridLayout : GridLayout {
+ fun splitIntoPages(
+ tiles: List<TileViewModel>,
+ rows: Int,
+ columns: Int,
+ ): List<List<TileViewModel>>
+
+ companion object {
+
+ /**
+ * Splits a list of [SizedTile] into rows, each with at most [columns] occupied.
+ *
+ * It will leave gaps at the end of a row if the next [SizedTile] has [SizedTile.width] that
+ * is larger than the space remaining in the row.
+ */
+ fun splitInRows(
+ tiles: List<SizedTile<TileViewModel>>,
+ columns: Int
+ ): List<List<SizedTile<TileViewModel>>> {
+ val row = TileRow<TileViewModel>(columns)
+
+ return buildList {
+ for (tile in tiles) {
+ check(tile.width <= columns)
+ if (!row.maybeAddTile(tile)) {
+ // Couldn't add tile to previous row, create a row with the current tiles
+ // and start a new one
+ add(row.tiles)
+ row.clear()
+ row.maybeAddTile(tile)
+ }
+ }
+ if (row.tiles.isNotEmpty()) {
+ add(row.tiles)
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
index 2f0fe22..ea97f0d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
@@ -26,9 +26,10 @@
import androidx.compose.ui.res.dimensionResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.res.R
@@ -39,13 +40,14 @@
@Inject
constructor(
private val iconTilesViewModel: IconTilesViewModel,
- private val gridSizeViewModel: InfiniteGridSizeViewModel,
-) : GridLayout {
+ private val gridSizeViewModel: FixedColumnsSizeViewModel,
+) : PaginatableGridLayout {
@Composable
override fun TileGrid(
tiles: List<TileViewModel>,
modifier: Modifier,
+ editModeStart: () -> Unit,
) {
DisposableEffect(tiles) {
val token = Any()
@@ -55,16 +57,8 @@
val columns by gridSizeViewModel.columns.collectAsStateWithLifecycle()
TileLazyGrid(modifier = modifier, columns = GridCells.Fixed(columns)) {
- items(
- tiles.size,
- span = { index ->
- if (iconTilesViewModel.isIconTile(tiles[index].spec)) {
- GridItemSpan(1)
- } else {
- GridItemSpan(2)
- }
- }
- ) { index ->
+ items(tiles.size, span = { index -> GridItemSpan(tiles[index].spec.width()) }) { index
+ ->
Tile(
tile = tiles[index],
iconOnly = iconTilesViewModel.isIconTile(tiles[index].spec),
@@ -92,4 +86,22 @@
onRemoveTile = onRemoveTile,
)
}
+
+ override fun splitIntoPages(
+ tiles: List<TileViewModel>,
+ rows: Int,
+ columns: Int,
+ ): List<List<TileViewModel>> {
+
+ return PaginatableGridLayout.splitInRows(
+ tiles.map { SizedTile(it, it.spec.width()) },
+ columns,
+ )
+ .chunked(rows)
+ .map { it.flatten().map { it.tile } }
+ }
+
+ private fun TileSpec.width(): Int {
+ return if (iconTilesViewModel.isIconTile(this)) 1 else 2
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PagerDots.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PagerDots.kt
new file mode 100644
index 0000000..7de22161
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PagerDots.kt
@@ -0,0 +1,166 @@
+/*
+ * 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.systemui.qs.panels.ui.compose
+
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.semantics.pageLeft
+import androidx.compose.ui.semantics.pageRight
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlin.math.absoluteValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+@Composable
+fun PagerDots(
+ pagerState: PagerState,
+ activeColor: Color,
+ nonActiveColor: Color,
+ modifier: Modifier = Modifier,
+ dotSize: Dp = 6.dp,
+ spaceSize: Dp = 4.dp,
+) {
+ if (pagerState.pageCount < 2) {
+ return
+ }
+ val inPageTransition by
+ remember(pagerState) {
+ derivedStateOf {
+ pagerState.currentPageOffsetFraction.absoluteValue > 0.01 &&
+ !pagerState.isOverscrolling()
+ }
+ }
+ val coroutineScope = rememberCoroutineScope()
+ Row(
+ modifier =
+ modifier
+ .wrapContentWidth()
+ .pagerDotsSemantics(
+ pagerState,
+ coroutineScope,
+ ),
+ horizontalArrangement = spacedBy(spaceSize),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (!inPageTransition) {
+ repeat(pagerState.pageCount) { i ->
+ // We use canvas directly to only invalidate the draw phase when the page is
+ // changing.
+ Canvas(Modifier.size(dotSize)) {
+ if (pagerState.currentPage == i) {
+ drawCircle(activeColor)
+ } else {
+ drawCircle(nonActiveColor)
+ }
+ }
+ }
+ } else {
+ val doubleDotWidth = dotSize * 2 + spaceSize
+ val cornerRadius = dotSize / 2
+ val width by
+ animateDpAsState(targetValue = if (inPageTransition) doubleDotWidth else dotSize)
+
+ fun DrawScope.drawDoubleRect() {
+ drawRoundRect(
+ color = activeColor,
+ size = Size(width.toPx(), dotSize.toPx()),
+ cornerRadius = CornerRadius(cornerRadius.toPx(), cornerRadius.toPx())
+ )
+ }
+
+ repeat(pagerState.pageCount) { page ->
+ Canvas(Modifier.size(dotSize)) {
+ val withPrevious = pagerState.currentPageOffsetFraction < 0
+ val ltr = layoutDirection == LayoutDirection.Ltr
+ if (
+ withPrevious && page == (pagerState.currentPage - 1) ||
+ !withPrevious && page == pagerState.currentPage
+ ) {
+ if (ltr) {
+ drawDoubleRect()
+ }
+ } else if (
+ withPrevious && page == pagerState.currentPage ||
+ !withPrevious && page == (pagerState.currentPage + 1)
+ ) {
+ if (!ltr) {
+ drawDoubleRect()
+ }
+ } else {
+ drawCircle(nonActiveColor)
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun Modifier.pagerDotsSemantics(
+ pagerState: PagerState,
+ coroutineScope: CoroutineScope,
+): Modifier {
+ return then(
+ Modifier.semantics {
+ pageLeft {
+ if (pagerState.canScrollBackward) {
+ coroutineScope.launch {
+ pagerState.animateScrollToPage(pagerState.currentPage - 1)
+ }
+ true
+ } else {
+ false
+ }
+ }
+ pageRight {
+ if (pagerState.canScrollForward) {
+ coroutineScope.launch {
+ pagerState.animateScrollToPage(pagerState.currentPage + 1)
+ }
+ true
+ } else {
+ false
+ }
+ }
+ stateDescription = "Page ${pagerState.settledPage + 1} of ${pagerState.pageCount}"
+ }
+ )
+}
+
+private fun PagerState.isOverscrolling(): Boolean {
+ val position = currentPage + currentPageOffsetFraction
+ return position < 0 || position > pageCount - 1
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
new file mode 100644
index 0000000..2ee957e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.systemui.qs.panels.ui.compose
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType
+import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.FooterHeight
+import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.InterPageSpacing
+import com.android.systemui.qs.panels.ui.viewmodel.PaginatedGridViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+class PaginatedGridLayout
+@Inject
+constructor(
+ private val viewModel: PaginatedGridViewModel,
+ @PaginatedBaseLayoutType private val delegateGridLayout: PaginatableGridLayout,
+) : GridLayout by delegateGridLayout {
+ @Composable
+ override fun TileGrid(
+ tiles: List<TileViewModel>,
+ modifier: Modifier,
+ editModeStart: () -> Unit,
+ ) {
+ DisposableEffect(tiles) {
+ val token = Any()
+ tiles.forEach { it.startListening(token) }
+ onDispose { tiles.forEach { it.stopListening(token) } }
+ }
+ val columns by viewModel.columns.collectAsStateWithLifecycle()
+ val rows by viewModel.rows.collectAsStateWithLifecycle()
+
+ val pages =
+ remember(tiles, columns, rows) {
+ delegateGridLayout.splitIntoPages(tiles, rows = rows, columns = columns)
+ }
+
+ val pagerState = rememberPagerState(0) { pages.size }
+
+ Column {
+ HorizontalPager(
+ state = pagerState,
+ modifier = Modifier,
+ pageSpacing = if (pages.size > 1) InterPageSpacing else 0.dp,
+ beyondViewportPageCount = 1,
+ verticalAlignment = Alignment.Top,
+ ) {
+ val page = pages[it]
+
+ delegateGridLayout.TileGrid(tiles = page, modifier = Modifier, editModeStart = {})
+ }
+ Box(
+ modifier = Modifier.height(FooterHeight).fillMaxWidth(),
+ ) {
+ PagerDots(
+ pagerState = pagerState,
+ activeColor = MaterialTheme.colorScheme.primary,
+ nonActiveColor = MaterialTheme.colorScheme.surfaceVariant,
+ modifier = Modifier.align(Alignment.Center)
+ )
+ CompositionLocalProvider(value = LocalContentColor provides Color.White) {
+ IconButton(
+ onClick = editModeStart,
+ modifier = Modifier.align(Alignment.CenterEnd),
+ ) {
+ Icon(
+ imageVector = Icons.Default.Edit,
+ contentDescription = stringResource(id = R.string.qs_edit)
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private object Dimensions {
+ val FooterHeight = 48.dp
+ val InterPageSpacing = 16.dp
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
index 9233e76..7f5e474 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PartitionedGridLayout.kt
@@ -53,6 +53,7 @@
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.modifiers.background
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.PartitionedGridViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
@@ -63,9 +64,13 @@
@SysUISingleton
class PartitionedGridLayout @Inject constructor(private val viewModel: PartitionedGridViewModel) :
- GridLayout {
+ PaginatableGridLayout {
@Composable
- override fun TileGrid(tiles: List<TileViewModel>, modifier: Modifier) {
+ override fun TileGrid(
+ tiles: List<TileViewModel>,
+ modifier: Modifier,
+ editModeStart: () -> Unit,
+ ) {
DisposableEffect(tiles) {
val token = Any()
tiles.forEach { it.startListening(token) }
@@ -169,6 +174,20 @@
}
}
+ override fun splitIntoPages(
+ tiles: List<TileViewModel>,
+ rows: Int,
+ columns: Int,
+ ): List<List<TileViewModel>> {
+ val (smallTiles, largeTiles) = tiles.partition { viewModel.isIconTile(it.spec) }
+
+ val sizedLargeTiles = largeTiles.map { SizedTile(it, 2) }
+ val sizedSmallTiles = smallTiles.map { SizedTile(it, 1) }
+ val largeTilesRows = PaginatableGridLayout.splitInRows(sizedLargeTiles, columns)
+ val smallTilesRows = PaginatableGridLayout.splitInRows(sizedSmallTiles, columns)
+ return (largeTilesRows + smallTilesRows).chunked(rows).map { it.flatten().map { it.tile } }
+ }
+
@Composable
private fun CurrentTiles(
tiles: List<EditTileViewModel>,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
index 7f4e0a7..4a90102 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/StretchedGridLayout.kt
@@ -30,8 +30,8 @@
import com.android.systemui.qs.panels.shared.model.SizedTile
import com.android.systemui.qs.panels.shared.model.TileRow
import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.FixedColumnsSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.res.R
@@ -42,13 +42,14 @@
@Inject
constructor(
private val iconTilesViewModel: IconTilesViewModel,
- private val gridSizeViewModel: InfiniteGridSizeViewModel,
+ private val gridSizeViewModel: FixedColumnsSizeViewModel,
) : GridLayout {
@Composable
override fun TileGrid(
tiles: List<TileViewModel>,
modifier: Modifier,
+ editModeStart: () -> Unit,
) {
DisposableEffect(tiles) {
val token = Any()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
index 2dab7c3..8c57d41 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileGrid.kt
@@ -23,9 +23,13 @@
import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel
@Composable
-fun TileGrid(viewModel: TileGridViewModel, modifier: Modifier = Modifier) {
+fun TileGrid(
+ viewModel: TileGridViewModel,
+ modifier: Modifier = Modifier,
+ editModeStart: () -> Unit
+) {
val gridLayout by viewModel.gridLayout.collectAsStateWithLifecycle()
val tiles by viewModel.tileViewModels.collectAsStateWithLifecycle(emptyList())
- gridLayout.TileGrid(tiles, modifier)
+ gridLayout.TileGrid(tiles, modifier, editModeStart)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModel.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModel.kt
index a4ee58f..865c86b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModel.kt
@@ -17,16 +17,16 @@
package com.android.systemui.qs.panels.ui.viewmodel
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.panels.domain.interactor.InfiniteGridSizeInteractor
+import com.android.systemui.qs.panels.domain.interactor.FixedColumnsSizeInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.StateFlow
-interface InfiniteGridSizeViewModel {
+interface FixedColumnsSizeViewModel {
val columns: StateFlow<Int>
}
@SysUISingleton
-class InfiniteGridSizeViewModelImpl @Inject constructor(interactor: InfiniteGridSizeInteractor) :
- InfiniteGridSizeViewModel {
+class FixedColumnsSizeViewModelImpl @Inject constructor(interactor: FixedColumnsSizeInteractor) :
+ FixedColumnsSizeViewModel {
override val columns: StateFlow<Int> = interactor.columns
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
new file mode 100644
index 0000000..28bf474
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.qs.panels.domain.interactor.PaginatedGridInteractor
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class PaginatedGridViewModel
+@Inject
+constructor(
+ iconTilesViewModel: IconTilesViewModel,
+ gridSizeViewModel: FixedColumnsSizeViewModel,
+ iconLabelVisibilityViewModel: IconLabelVisibilityViewModel,
+ paginatedGridInteractor: PaginatedGridInteractor,
+ @Application applicationScope: CoroutineScope,
+) :
+ IconTilesViewModel by iconTilesViewModel,
+ FixedColumnsSizeViewModel by gridSizeViewModel,
+ IconLabelVisibilityViewModel by iconLabelVisibilityViewModel {
+ val rows =
+ paginatedGridInteractor.rows.stateIn(
+ applicationScope,
+ SharingStarted.WhileSubscribed(),
+ paginatedGridInteractor.defaultRows,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt
index 730cf63..2049edb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModel.kt
@@ -24,9 +24,9 @@
@Inject
constructor(
iconTilesViewModel: IconTilesViewModel,
- gridSizeViewModel: InfiniteGridSizeViewModel,
+ gridSizeViewModel: FixedColumnsSizeViewModel,
iconLabelVisibilityViewModel: IconLabelVisibilityViewModel,
) :
IconTilesViewModel by iconTilesViewModel,
- InfiniteGridSizeViewModel by gridSizeViewModel,
+ FixedColumnsSizeViewModel by gridSizeViewModel,
IconLabelVisibilityViewModel by iconLabelVisibilityViewModel
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
index 70f3b84..a3feb2b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
@@ -46,6 +46,7 @@
import com.android.systemui.recordissue.IssueRecordingService
import com.android.systemui.recordissue.IssueRecordingState
import com.android.systemui.recordissue.RecordIssueDialogDelegate
+import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC
import com.android.systemui.recordissue.TraceurMessageSender
import com.android.systemui.res.R
import com.android.systemui.screenrecord.RecordingService
@@ -197,8 +198,4 @@
expandedAccessibilityClassName = Switch::class.java.name
}
}
-
- companion object {
- const val TILE_SPEC = "record_issue"
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractor.kt
new file mode 100644
index 0000000..1af328e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingDataInteractor.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.systemui.qs.tiles.impl.irecording
+
+import android.os.UserHandle
+import com.android.systemui.Flags
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.recordissue.IssueRecordingState
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.onStart
+
+class IssueRecordingDataInteractor
+@Inject
+constructor(
+ private val state: IssueRecordingState,
+ @Background private val bgCoroutineContext: CoroutineContext,
+) : QSTileDataInteractor<IssueRecordingModel> {
+
+ override fun tileData(
+ user: UserHandle,
+ triggers: Flow<DataUpdateTrigger>
+ ): Flow<IssueRecordingModel> =
+ conflatedCallbackFlow {
+ val listener = Runnable { trySend(IssueRecordingModel(state.isRecording)) }
+ state.addListener(listener)
+ awaitClose { state.removeListener(listener) }
+ }
+ .onStart { emit(IssueRecordingModel(state.isRecording)) }
+ .distinctUntilChanged()
+ .flowOn(bgCoroutineContext)
+
+ override fun availability(user: UserHandle): Flow<Boolean> =
+ flowOf(android.os.Build.IS_DEBUGGABLE && Flags.recordIssueQsTile())
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt
new file mode 100644
index 0000000..ff931b3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.systemui.qs.tiles.impl.irecording
+
+import android.content.res.Resources
+import android.content.res.Resources.Theme
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+class IssueRecordingMapper
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ private val theme: Theme,
+) : QSTileDataToStateMapper<IssueRecordingModel> {
+ override fun map(config: QSTileConfig, data: IssueRecordingModel): QSTileState =
+ QSTileState.build(resources, theme, config.uiConfig) {
+ if (data.isRecording) {
+ activationState = QSTileState.ActivationState.ACTIVE
+ secondaryLabel = resources.getString(R.string.qs_record_issue_stop)
+ icon = { Icon.Resource(R.drawable.qs_record_issue_icon_on, null) }
+ } else {
+ icon = { Icon.Resource(R.drawable.qs_record_issue_icon_off, null) }
+ activationState = QSTileState.ActivationState.INACTIVE
+ secondaryLabel = resources.getString(R.string.qs_record_issue_start)
+ }
+ supportedActions = setOf(QSTileState.UserAction.CLICK)
+ contentDescription = "$label, $secondaryLabel"
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingModel.kt
similarity index 76%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
copy to packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingModel.kt
index d8af3fa..260729b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingModel.kt
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-package com.android.systemui.qs.panels.data.repository
+package com.android.systemui.qs.tiles.impl.irecording
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.infiniteGridSizeRepository by Kosmos.Fixture { InfiniteGridSizeRepository() }
+@JvmInline value class IssueRecordingModel(val isRecording: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt
new file mode 100644
index 0000000..4971fef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingUserActionInteractor.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.systemui.qs.tiles.impl.irecording
+
+import android.app.AlertDialog
+import android.app.BroadcastOptions
+import android.app.PendingIntent
+import android.util.Log
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.animation.DialogCuj
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.animation.Expandable
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.recordissue.IssueRecordingService
+import com.android.systemui.recordissue.RecordIssueDialogDelegate
+import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC
+import com.android.systemui.screenrecord.RecordingService
+import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.withContext
+
+private const val TAG = "IssueRecordingActionInteractor"
+
+class IssueRecordingUserActionInteractor
+@Inject
+constructor(
+ @Main private val mainCoroutineContext: CoroutineContext,
+ private val keyguardDismissUtil: KeyguardDismissUtil,
+ private val keyguardStateController: KeyguardStateController,
+ private val dialogTransitionAnimator: DialogTransitionAnimator,
+ private val panelInteractor: PanelInteractor,
+ private val userContextProvider: UserContextProvider,
+ private val delegateFactory: RecordIssueDialogDelegate.Factory,
+) : QSTileUserActionInteractor<IssueRecordingModel> {
+
+ override suspend fun handleInput(input: QSTileInput<IssueRecordingModel>) {
+ if (input.action is QSTileUserAction.Click) {
+ if (input.data.isRecording) {
+ stopIssueRecordingService()
+ } else {
+ withContext(mainCoroutineContext) { showPrompt(input.action.expandable) }
+ }
+ } else {
+ Log.v(TAG, "the RecordIssueTile doesn't handle ${input.action} events yet.")
+ }
+ }
+
+ private fun showPrompt(expandable: Expandable?) {
+ val dialog: AlertDialog =
+ delegateFactory
+ .create {
+ startIssueRecordingService()
+ dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations()
+ panelInteractor.collapsePanels()
+ }
+ .createDialog()
+ val dismissAction =
+ ActivityStarter.OnDismissAction {
+ // We animate from the touched view only if we are not on the keyguard, given
+ // that if we are we will dismiss it which will also collapse the shade.
+ if (expandable != null && !keyguardStateController.isShowing) {
+ expandable
+ .dialogTransitionController(
+ DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, TILE_SPEC)
+ )
+ ?.let { dialogTransitionAnimator.show(dialog, it) } ?: dialog.show()
+ } else {
+ dialog.show()
+ }
+ false
+ }
+ keyguardDismissUtil.executeWhenUnlocked(dismissAction, false, true)
+ }
+
+ private fun startIssueRecordingService() =
+ PendingIntent.getForegroundService(
+ userContextProvider.userContext,
+ RecordingService.REQUEST_CODE,
+ IssueRecordingService.getStartIntent(userContextProvider.userContext),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle())
+
+ private fun stopIssueRecordingService() =
+ PendingIntent.getService(
+ userContextProvider.userContext,
+ RecordingService.REQUEST_CODE,
+ IssueRecordingService.getStopIntent(userContextProvider.userContext),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle())
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
index 4ea3345..b077349 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt
@@ -18,7 +18,7 @@
import android.content.Context
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.qs.tiles.RecordIssueTile
+import com.android.systemui.recordissue.RecordIssueModule.Companion.TILE_SPEC
import com.android.systemui.res.R
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
@@ -35,11 +35,7 @@
) {
private val prefs =
- userFileManager.getSharedPreferences(
- RecordIssueTile.TILE_SPEC,
- Context.MODE_PRIVATE,
- userTracker.userId
- )
+ userFileManager.getSharedPreferences(TILE_SPEC, Context.MODE_PRIVATE, userTracker.userId)
var takeBugreport
get() = prefs.getBoolean(KEY_TAKE_BUG_REPORT, false)
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt
index 26af9a7..907b92c 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueModule.kt
@@ -16,12 +16,20 @@
package com.android.systemui.recordissue
+import com.android.systemui.Flags
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.qs.tiles.RecordIssueTile
+import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
+import com.android.systemui.qs.tiles.impl.irecording.IssueRecordingDataInteractor
+import com.android.systemui.qs.tiles.impl.irecording.IssueRecordingMapper
+import com.android.systemui.qs.tiles.impl.irecording.IssueRecordingModel
+import com.android.systemui.qs.tiles.impl.irecording.IssueRecordingUserActionInteractor
import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
+import com.android.systemui.qs.tiles.viewmodel.StubQSTileViewModel
import com.android.systemui.res.R
import dagger.Binds
import dagger.Module
@@ -34,19 +42,19 @@
/** Inject RecordIssueTile into tileMap in QSModule */
@Binds
@IntoMap
- @StringKey(RecordIssueTile.TILE_SPEC)
+ @StringKey(TILE_SPEC)
fun bindRecordIssueTile(recordIssueTile: RecordIssueTile): QSTileImpl<*>
companion object {
- const val RECORD_ISSUE_TILE_SPEC = "record_issue"
+ const val TILE_SPEC = "record_issue"
@Provides
@IntoMap
- @StringKey(RECORD_ISSUE_TILE_SPEC)
+ @StringKey(TILE_SPEC)
fun provideRecordIssueTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
QSTileConfig(
- tileSpec = TileSpec.create(RECORD_ISSUE_TILE_SPEC),
+ tileSpec = TileSpec.create(TILE_SPEC),
uiConfig =
QSTileUIConfig.Resource(
iconRes = R.drawable.qs_record_issue_icon_off,
@@ -54,5 +62,24 @@
),
instanceId = uiEventLogger.getNewInstanceId(),
)
+
+ /** Inject FlashlightTile into tileViewModelMap in QSModule */
+ @Provides
+ @IntoMap
+ @StringKey(TILE_SPEC)
+ fun provideIssueRecordingTileViewModel(
+ factory: QSTileViewModelFactory.Static<IssueRecordingModel>,
+ mapper: IssueRecordingMapper,
+ stateInteractor: IssueRecordingDataInteractor,
+ userActionInteractor: IssueRecordingUserActionInteractor
+ ): QSTileViewModel =
+ if (Flags.qsNewTilesFuture())
+ factory.create(
+ TileSpec.create(TILE_SPEC),
+ userActionInteractor,
+ stateInteractor,
+ mapper,
+ )
+ else StubQSTileViewModel
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
index ac91337..9f48ee9 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
@@ -64,14 +64,25 @@
// TODO(b/338577208): Remove this once we add Dual Shade invocation zones.
shadeMode is ShadeMode.Dual
) {
- put(
- Swipe(
- pointerCount = 2,
- fromSource = Edge.Top,
- direction = SwipeDirection.Down,
- ),
- UserActionResult(SceneFamilies.QuickSettings)
- )
+ if (shadeInteractor.shadeAlignment == Alignment.BottomEnd) {
+ put(
+ Swipe(
+ pointerCount = 2,
+ fromSource = Edge.Bottom,
+ direction = SwipeDirection.Up,
+ ),
+ UserActionResult(SceneFamilies.QuickSettings, OpenBottomShade)
+ )
+ } else {
+ put(
+ Swipe(
+ pointerCount = 2,
+ fromSource = Edge.Top,
+ direction = SwipeDirection.Down,
+ ),
+ UserActionResult(SceneFamilies.QuickSettings)
+ )
+ }
}
if (shadeInteractor.shadeAlignment == Alignment.BottomEnd) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index ee8161c..ce321dc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -182,7 +182,11 @@
}
override fun expandToQs() {
- sceneInteractor.changeScene(SceneFamilies.QuickSettings, "ShadeController.animateExpandQs")
+ sceneInteractor.changeScene(
+ SceneFamilies.QuickSettings,
+ "ShadeController.animateExpandQs",
+ OpenBottomShade.takeIf { shadeInteractor.shadeAlignment == Alignment.BottomEnd }
+ )
}
override fun setVisibilityListener(listener: ShadeVisibilityListener) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
index b946129..6551854 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
@@ -51,7 +51,7 @@
initialValue = Scenes.Lockscreen,
)
- /** Dictates whether the panel is aligned to the top or the bottom. */
+ /** Dictates the alignment of the overlay shade panel on the screen. */
val panelAlignment = shadeInteractor.shadeAlignment
/** Notifies that the user has clicked the semi-transparent background scrim. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
index 855798c..28e3a83 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java
@@ -94,7 +94,6 @@
private float mCornerAnimationDistance;
private float mActualWidth = -1;
private int mMaxIconsOnLockscreen;
- private int mNotificationScrimPadding;
private boolean mCanModifyColorOfNotifications;
private boolean mCanInteract;
private NotificationStackScrollLayout mHostLayout;
@@ -138,7 +137,6 @@
mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext);
mPaddingBetweenElements = res.getDimensionPixelSize(R.dimen.notification_divider_height);
mMaxIconsOnLockscreen = res.getInteger(R.integer.max_notif_icons_on_lockscreen);
- mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
ViewGroup.LayoutParams layoutParams = getLayoutParams();
final int newShelfHeight = res.getDimensionPixelOffset(R.dimen.notification_shelf_height);
@@ -265,7 +263,7 @@
}
final float stackBottom = SceneContainerFlag.isEnabled()
- ? getStackBottom(ambientState)
+ ? ambientState.getStackTop() + ambientState.getStackHeight()
: ambientState.getStackY() + ambientState.getStackHeight();
if (viewState.hidden) {
@@ -278,19 +276,6 @@
}
}
- /**
- * bottom-most position, where we can draw the stack
- */
- private float getStackBottom(AmbientState ambientState) {
- if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
- float stackBottom = ambientState.getStackCutoff() - mNotificationScrimPadding;
- if (ambientState.isExpansionChanging()) {
- stackBottom = MathUtils.lerp(stackBottom * StackScrollAlgorithm.START_FRACTION,
- stackBottom, ambientState.getExpansionFraction());
- }
- return stackBottom;
- }
-
private int getSpeedBumpIndex() {
NotificationIconContainerRefactor.assertInLegacyMode();
return mHostLayout.getSpeedBumpIndex();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index e704140..5e08b0b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -46,6 +46,10 @@
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor
import com.android.systemui.statusbar.notification.InflationException
import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED
+import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED
+import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP
+import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_SINGLELINE
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED
@@ -255,39 +259,29 @@
) {
when (inflateFlag) {
FLAG_CONTENT_VIEW_CONTRACTED ->
- row.privateLayout.performWhenContentInactive(
- NotificationContentView.VISIBLE_TYPE_CONTRACTED
- ) {
+ row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_CONTRACTED) {
row.privateLayout.setContractedChild(null)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)
}
FLAG_CONTENT_VIEW_EXPANDED ->
- row.privateLayout.performWhenContentInactive(
- NotificationContentView.VISIBLE_TYPE_EXPANDED
- ) {
+ row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_EXPANDED) {
row.privateLayout.setExpandedChild(null)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
}
FLAG_CONTENT_VIEW_HEADS_UP ->
- row.privateLayout.performWhenContentInactive(
- NotificationContentView.VISIBLE_TYPE_HEADSUP
- ) {
+ row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_HEADSUP) {
row.privateLayout.setHeadsUpChild(null)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
row.privateLayout.setHeadsUpInflatedSmartReplies(null)
}
FLAG_CONTENT_VIEW_PUBLIC ->
- row.publicLayout.performWhenContentInactive(
- NotificationContentView.VISIBLE_TYPE_CONTRACTED
- ) {
+ row.publicLayout.performWhenContentInactive(VISIBLE_TYPE_CONTRACTED) {
row.publicLayout.setContractedChild(null)
remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)
}
FLAG_CONTENT_VIEW_SINGLE_LINE -> {
if (AsyncHybridViewInflation.isEnabled) {
- row.privateLayout.performWhenContentInactive(
- NotificationContentView.VISIBLE_TYPE_SINGLELINE
- ) {
+ row.privateLayout.performWhenContentInactive(VISIBLE_TYPE_SINGLELINE) {
row.privateLayout.setSingleLineView(null)
}
}
@@ -308,32 +302,22 @@
@InflationFlag contentViews: Int
) {
if (contentViews and FLAG_CONTENT_VIEW_CONTRACTED != 0) {
- row.privateLayout.removeContentInactiveRunnable(
- NotificationContentView.VISIBLE_TYPE_CONTRACTED
- )
+ row.privateLayout.removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED)
}
if (contentViews and FLAG_CONTENT_VIEW_EXPANDED != 0) {
- row.privateLayout.removeContentInactiveRunnable(
- NotificationContentView.VISIBLE_TYPE_EXPANDED
- )
+ row.privateLayout.removeContentInactiveRunnable(VISIBLE_TYPE_EXPANDED)
}
if (contentViews and FLAG_CONTENT_VIEW_HEADS_UP != 0) {
- row.privateLayout.removeContentInactiveRunnable(
- NotificationContentView.VISIBLE_TYPE_HEADSUP
- )
+ row.privateLayout.removeContentInactiveRunnable(VISIBLE_TYPE_HEADSUP)
}
if (contentViews and FLAG_CONTENT_VIEW_PUBLIC != 0) {
- row.publicLayout.removeContentInactiveRunnable(
- NotificationContentView.VISIBLE_TYPE_CONTRACTED
- )
+ row.publicLayout.removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED)
}
if (
AsyncHybridViewInflation.isEnabled &&
contentViews and FLAG_CONTENT_VIEW_SINGLE_LINE != 0
) {
- row.privateLayout.removeContentInactiveRunnable(
- NotificationContentView.VISIBLE_TYPE_SINGLELINE
- )
+ row.privateLayout.removeContentInactiveRunnable(VISIBLE_TYPE_SINGLELINE)
}
}
@@ -847,10 +831,7 @@
callback = callback,
parentLayout = privateLayout,
existingView = privateLayout.contractedChild,
- existingWrapper =
- privateLayout.getVisibleWrapper(
- NotificationContentView.VISIBLE_TYPE_CONTRACTED
- ),
+ existingWrapper = privateLayout.getVisibleWrapper(VISIBLE_TYPE_CONTRACTED),
runningInflations = runningInflations,
applyCallback = applyCallback,
logger = logger
@@ -891,10 +872,7 @@
callback = callback,
parentLayout = privateLayout,
existingView = privateLayout.expandedChild,
- existingWrapper =
- privateLayout.getVisibleWrapper(
- NotificationContentView.VISIBLE_TYPE_EXPANDED
- ),
+ existingWrapper = privateLayout.getVisibleWrapper(VISIBLE_TYPE_EXPANDED),
runningInflations = runningInflations,
applyCallback = applyCallback,
logger = logger
@@ -936,10 +914,7 @@
callback = callback,
parentLayout = privateLayout,
existingView = privateLayout.headsUpChild,
- existingWrapper =
- privateLayout.getVisibleWrapper(
- NotificationContentView.VISIBLE_TYPE_HEADSUP
- ),
+ existingWrapper = privateLayout.getVisibleWrapper(VISIBLE_TYPE_HEADSUP),
runningInflations = runningInflations,
applyCallback = applyCallback,
logger = logger
@@ -979,10 +954,7 @@
callback = callback,
parentLayout = publicLayout,
existingView = publicLayout.contractedChild,
- existingWrapper =
- publicLayout.getVisibleWrapper(
- NotificationContentView.VISIBLE_TYPE_CONTRACTED
- ),
+ existingWrapper = publicLayout.getVisibleWrapper(VISIBLE_TYPE_CONTRACTED),
runningInflations = runningInflations,
applyCallback = applyCallback,
logger = logger
@@ -1359,79 +1331,17 @@
if (runningInflations.isNotEmpty()) {
return false
}
- val privateLayout = row.privateLayout
- val publicLayout = row.publicLayout
logger.logAsyncTaskProgress(entry, "finishing")
- if (reInflateFlags and FLAG_CONTENT_VIEW_CONTRACTED != 0) {
- if (result.inflatedContentView != null) {
- // New view case
- privateLayout.setContractedChild(result.inflatedContentView)
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_CONTRACTED,
- result.remoteViews.contracted
- )
- } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_CONTRACTED)) {
- // Reinflation case. Only update if it's still cached (i.e. view has not been
- // freed while inflating).
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_CONTRACTED,
- result.remoteViews.contracted
- )
- }
- }
- if (reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0) {
- if (result.inflatedExpandedView != null) {
- privateLayout.setExpandedChild(result.inflatedExpandedView)
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_EXPANDED,
- result.remoteViews.expanded
- )
- } else if (result.remoteViews.expanded == null) {
- privateLayout.setExpandedChild(null)
- remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)
- } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_EXPANDED)) {
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_EXPANDED,
- result.remoteViews.expanded
- )
- }
- if (result.remoteViews.expanded != null) {
- privateLayout.setExpandedInflatedSmartReplies(
- result.expandedInflatedSmartReplies
- )
- } else {
- privateLayout.setExpandedInflatedSmartReplies(null)
- }
- row.setExpandable(result.remoteViews.expanded != null)
- }
- if (reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0) {
- if (result.inflatedHeadsUpView != null) {
- privateLayout.setHeadsUpChild(result.inflatedHeadsUpView)
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_HEADS_UP,
- result.remoteViews.headsUp
- )
- } else if (result.remoteViews.headsUp == null) {
- privateLayout.setHeadsUpChild(null)
- remoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)
- } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_HEADS_UP)) {
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_HEADS_UP,
- result.remoteViews.headsUp
- )
- }
- if (result.remoteViews.headsUp != null) {
- privateLayout.setHeadsUpInflatedSmartReplies(result.headsUpInflatedSmartReplies)
- } else {
- privateLayout.setHeadsUpInflatedSmartReplies(null)
- }
- }
+ setViewsFromRemoteViews(
+ reInflateFlags,
+ entry,
+ remoteViewCache,
+ result,
+ row,
+ isMinimized,
+ )
+ result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) }
+
if (
AsyncHybridViewInflation.isEnabled &&
reInflateFlags and FLAG_CONTENT_VIEW_SINGLE_LINE != 0
@@ -1444,72 +1354,7 @@
} else {
SingleLineViewBinder.bind(viewModel, singleLineView)
}
- privateLayout.setSingleLineView(result.inflatedSingleLineView)
- }
- }
- result.inflatedSmartReplyState?.let { privateLayout.setInflatedSmartReplyState(it) }
- if (reInflateFlags and FLAG_CONTENT_VIEW_PUBLIC != 0) {
- if (result.inflatedPublicView != null) {
- publicLayout.setContractedChild(result.inflatedPublicView)
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_PUBLIC,
- result.remoteViews.public
- )
- } else if (remoteViewCache.hasCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC)) {
- remoteViewCache.putCachedView(
- entry,
- FLAG_CONTENT_VIEW_PUBLIC,
- result.remoteViews.public
- )
- }
- }
- if (AsyncGroupHeaderViewInflation.isEnabled) {
- if (reInflateFlags and FLAG_GROUP_SUMMARY_HEADER != 0) {
- if (result.inflatedGroupHeaderView != null) {
- // We need to set if the row is minimized before setting the group header to
- // make sure the setting of header view works correctly
- row.setIsMinimized(isMinimized)
- row.setGroupHeader(/* headerView= */ result.inflatedGroupHeaderView)
- remoteViewCache.putCachedView(
- entry,
- FLAG_GROUP_SUMMARY_HEADER,
- result.remoteViews.normalGroupHeader
- )
- } else if (remoteViewCache.hasCachedView(entry, FLAG_GROUP_SUMMARY_HEADER)) {
- // Re-inflation case. Only update if it's still cached (i.e. view has not
- // been freed while inflating).
- remoteViewCache.putCachedView(
- entry,
- FLAG_GROUP_SUMMARY_HEADER,
- result.remoteViews.normalGroupHeader
- )
- }
- }
- if (reInflateFlags and FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER != 0) {
- if (result.inflatedMinimizedGroupHeaderView != null) {
- // We need to set if the row is minimized before setting the group header to
- // make sure the setting of header view works correctly
- row.setIsMinimized(isMinimized)
- row.setMinimizedGroupHeader(
- /* headerView= */ result.inflatedMinimizedGroupHeaderView
- )
- remoteViewCache.putCachedView(
- entry,
- FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
- result.remoteViews.minimizedGroupHeader
- )
- } else if (
- remoteViewCache.hasCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)
- ) {
- // Re-inflation case. Only update if it's still cached (i.e. view has not
- // been freed while inflating).
- remoteViewCache.putCachedView(
- entry,
- FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
- result.remoteViews.normalGroupHeader
- )
- }
+ row.privateLayout.setSingleLineView(result.inflatedSingleLineView)
}
}
entry.setContentModel(result.contentModel)
@@ -1518,6 +1363,120 @@
return true
}
+ private fun setViewsFromRemoteViews(
+ reInflateFlags: Int,
+ entry: NotificationEntry,
+ remoteViewCache: NotifRemoteViewCache,
+ result: InflationProgress,
+ row: ExpandableNotificationRow,
+ isMinimized: Boolean,
+ ) {
+ val privateLayout = row.privateLayout
+ val publicLayout = row.publicLayout
+ val remoteViewsUpdater = RemoteViewsUpdater(reInflateFlags, entry, remoteViewCache)
+ remoteViewsUpdater.setContentView(
+ FLAG_CONTENT_VIEW_CONTRACTED,
+ result.remoteViews.contracted,
+ result.inflatedContentView,
+ privateLayout::setContractedChild
+ )
+ remoteViewsUpdater.setContentView(
+ FLAG_CONTENT_VIEW_EXPANDED,
+ result.remoteViews.expanded,
+ result.inflatedExpandedView,
+ privateLayout::setExpandedChild
+ )
+ remoteViewsUpdater.setSmartReplies(
+ FLAG_CONTENT_VIEW_EXPANDED,
+ result.remoteViews.expanded,
+ result.expandedInflatedSmartReplies,
+ privateLayout::setExpandedInflatedSmartReplies
+ )
+ if (reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0) {
+ row.setExpandable(result.remoteViews.expanded != null)
+ }
+ remoteViewsUpdater.setContentView(
+ FLAG_CONTENT_VIEW_HEADS_UP,
+ result.remoteViews.headsUp,
+ result.inflatedHeadsUpView,
+ privateLayout::setHeadsUpChild
+ )
+ remoteViewsUpdater.setSmartReplies(
+ FLAG_CONTENT_VIEW_HEADS_UP,
+ result.remoteViews.headsUp,
+ result.headsUpInflatedSmartReplies,
+ privateLayout::setHeadsUpInflatedSmartReplies
+ )
+ remoteViewsUpdater.setContentView(
+ FLAG_CONTENT_VIEW_PUBLIC,
+ result.remoteViews.public,
+ result.inflatedPublicView,
+ publicLayout::setContractedChild
+ )
+ if (AsyncGroupHeaderViewInflation.isEnabled) {
+ remoteViewsUpdater.setContentView(
+ FLAG_GROUP_SUMMARY_HEADER,
+ result.remoteViews.normalGroupHeader,
+ result.inflatedGroupHeaderView,
+ ) { views ->
+ row.setIsMinimized(isMinimized)
+ row.setGroupHeader(views)
+ }
+ remoteViewsUpdater.setContentView(
+ FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER,
+ result.remoteViews.minimizedGroupHeader,
+ result.inflatedMinimizedGroupHeaderView,
+ ) { views ->
+ row.setIsMinimized(isMinimized)
+ row.setMinimizedGroupHeader(views)
+ }
+ }
+ }
+
+ private class RemoteViewsUpdater(
+ @InflationFlag private val reInflateFlags: Int,
+ private val entry: NotificationEntry,
+ private val remoteViewCache: NotifRemoteViewCache,
+ ) {
+ fun <V : View> setContentView(
+ @InflationFlag flagState: Int,
+ remoteViews: RemoteViews?,
+ view: V?,
+ setView: (V?) -> Unit,
+ ) {
+ val clearViewFlags = FLAG_CONTENT_VIEW_HEADS_UP or FLAG_CONTENT_VIEW_EXPANDED
+ val shouldClearView = flagState and clearViewFlags != 0
+ if (reInflateFlags and flagState != 0) {
+ if (view != null) {
+ setView(view)
+ remoteViewCache.putCachedView(entry, flagState, remoteViews)
+ } else if (shouldClearView && remoteViews == null) {
+ setView(null)
+ remoteViewCache.removeCachedView(entry, flagState)
+ } else if (remoteViewCache.hasCachedView(entry, flagState)) {
+ // Re-inflation case. Only update if it's still cached (i.e. view has not
+ // been freed while inflating).
+ remoteViewCache.putCachedView(entry, flagState, remoteViews)
+ }
+ }
+ }
+
+ fun setSmartReplies(
+ @InflationFlag flagState: Int,
+ remoteViews: RemoteViews?,
+ smartReplies: InflatedSmartReplyViewHolder?,
+ setSmartReplies: (InflatedSmartReplyViewHolder?) -> Unit,
+ ) {
+ if (reInflateFlags and flagState != 0) {
+ if (remoteViews != null) {
+ setSmartReplies(smartReplies)
+ } else {
+ setSmartReplies(null)
+ }
+ }
+ }
+ }
+
private fun createExpandedView(
builder: Notification.Builder,
isMinimized: Boolean
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 2cbb6ae..fbddc06 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -131,13 +131,13 @@
/** Distance of top of notifications panel from top of screen. */
private float mStackY = 0;
- /** Height of notifications panel. */
+ /** Height of notifications panel interpolated by the expansion fraction. */
private float mStackHeight = 0;
/** Fraction of shade expansion. */
private float mExpansionFraction;
- /** Height of the notifications panel without top padding when expansion completes. */
+ /** Height of the notifications panel when expansion completes. */
private float mStackEndHeight;
/** Whether we are swiping up. */
@@ -176,8 +176,7 @@
}
/**
- * @param stackEndHeight Height of the notifications panel without top padding
- * when expansion completes.
+ * @see #getStackEndHeight()
*/
public void setStackEndHeight(float stackEndHeight) {
mStackEndHeight = stackEndHeight;
@@ -257,14 +256,14 @@
}
/**
- * @param stackHeight Height of notifications panel.
+ * @see #getStackHeight()
*/
public void setStackHeight(float stackHeight) {
mStackHeight = stackHeight;
}
/**
- * @return Height of notifications panel.
+ * @return Height of notifications panel interpolated by the expansion fraction.
*/
public float getStackHeight() {
return mStackHeight;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 1eee466..d54e66e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1440,22 +1440,40 @@
@VisibleForTesting
public void updateStackEndHeightAndStackHeight(float fraction) {
final float oldStackHeight = mAmbientState.getStackHeight();
- if (mQsExpansionFraction <= 0 && !shouldSkipHeightUpdate()) {
- final float endHeight = updateStackEndHeight(
- getHeight(), getEmptyBottomMargin(), getTopPadding());
+ if (SceneContainerFlag.isEnabled()) {
+ final float endHeight;
+ if (!shouldSkipHeightUpdate()) {
+ endHeight = updateStackEndHeight();
+ } else {
+ endHeight = mAmbientState.getStackEndHeight();
+ }
updateStackHeight(endHeight, fraction);
} else {
- // Always updateStackHeight to prevent jumps in the stack height when this fraction
- // suddenly reapplies after a freeze.
- final float endHeight = mAmbientState.getStackEndHeight();
- updateStackHeight(endHeight, fraction);
+ if (mQsExpansionFraction <= 0 && !shouldSkipHeightUpdate()) {
+ final float endHeight = updateStackEndHeight(
+ getHeight(), getEmptyBottomMargin(), getTopPadding());
+ updateStackHeight(endHeight, fraction);
+ } else {
+ // Always updateStackHeight to prevent jumps in the stack height when this fraction
+ // suddenly reapplies after a freeze.
+ final float endHeight = mAmbientState.getStackEndHeight();
+ updateStackHeight(endHeight, fraction);
+ }
}
if (oldStackHeight != mAmbientState.getStackHeight()) {
requestChildrenUpdate();
}
}
+ private float updateStackEndHeight() {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+ float height = Math.max(0f, mAmbientState.getStackCutoff() - mAmbientState.getStackTop());
+ mAmbientState.setStackEndHeight(height);
+ return height;
+ }
+
private float updateStackEndHeight(float height, float bottomMargin, float topPadding) {
+ SceneContainerFlag.assertInLegacyMode();
final float stackEndHeight;
if (mMaxDisplayedNotifications != -1) {
// The stack intrinsic height already contains the correct value when there is a limit
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index ca74c0e..ee7b5c4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -445,12 +445,26 @@
state.visibleChildren.clear();
state.visibleChildren.ensureCapacity(childCount);
int notGoneIndex = 0;
+ boolean emptyShadeVisible = false;
for (int i = 0; i < childCount; i++) {
ExpandableView v = (ExpandableView) mHostView.getChildAt(i);
if (v.getVisibility() != View.GONE) {
if (v == ambientState.getShelf()) {
continue;
}
+ if (FooterViewRefactor.isEnabled()) {
+ if (v instanceof EmptyShadeView) {
+ emptyShadeVisible = true;
+ }
+ if (v instanceof FooterView) {
+ if (emptyShadeVisible || notGoneIndex == 0) {
+ // if the empty shade is visible or the footer is the first visible
+ // view, we're in a transitory state so let's leave the footer alone.
+ continue;
+ }
+ }
+ }
+
notGoneIndex = updateNotGoneIndex(state, notGoneIndex, v);
if (v instanceof ExpandableNotificationRow row) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
index 503c52f..ce1a885 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
@@ -17,7 +17,6 @@
package com.android.systemui.recordissue
import android.app.Dialog
-import android.content.SharedPreferences
import android.os.UserHandle
import android.testing.TestableLooper
import android.widget.Button
@@ -57,6 +56,7 @@
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -71,7 +71,6 @@
@Mock private lateinit var mediaProjectionMetricsLogger: MediaProjectionMetricsLogger
@Mock private lateinit var userTracker: UserTracker
@Mock private lateinit var state: IssueRecordingState
- @Mock private lateinit var sharedPreferences: SharedPreferences
@Mock
private lateinit var screenCaptureDisabledDialogDelegate: ScreenCaptureDisabledDialogDelegate
@Mock private lateinit var screenCaptureDisabledDialog: SystemUIDialog
@@ -192,7 +191,7 @@
anyInt(),
eq(SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER)
)
- verify(factory).create(any<ScreenCapturePermissionDialogDelegate>())
+ verify(factory, times(2)).create(any(SystemUIDialog.Delegate::class.java))
}
@Test
@@ -213,7 +212,7 @@
anyInt(),
eq(SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER)
)
- verify(factory, never()).create(any<ScreenCapturePermissionDialogDelegate>())
+ verify(factory, never()).create()
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
index 9b0fd96..c1f2cb77 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationShelfTest.kt
@@ -367,14 +367,14 @@
@EnableSceneContainer
fun updateState_withViewInShelf_showShelf() {
// GIVEN a view is scrolled into the shelf
- val stackCutoff = 200f
- val scrimPadding =
- context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
- val shelfTop = stackCutoff - scrimPadding - shelf.height
+ val stackTop = 200f
+ val stackHeight = 800f
+ whenever(ambientState.stackTop).thenReturn(stackTop)
+ whenever(ambientState.stackHeight).thenReturn(stackHeight)
+ val shelfTop = stackTop + stackHeight - shelf.height
val stackScrollAlgorithmState = StackScrollAlgorithmState()
val viewInShelf = mock(ExpandableView::class.java)
- whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
whenever(ambientState.isShadeExpanded).thenReturn(true)
whenever(ambientState.lastVisibleBackgroundChild).thenReturn(viewInShelf)
whenever(viewInShelf.viewState).thenReturn(ExpandableViewState())
@@ -401,57 +401,14 @@
@Test
@EnableSceneContainer
- fun updateState_withViewInShelfDuringExpansion_showShelf() {
- // GIVEN a view is scrolled into the shelf
- val stackCutoff = 200f
- val scrimPadding =
- context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
- val stackBottom = stackCutoff - scrimPadding
- val shelfTop = stackBottom - shelf.height
- val stackScrollAlgorithmState = StackScrollAlgorithmState()
- val viewInShelf = mock(ExpandableView::class.java)
-
- // AND a shade expansion is in progress
- val shadeExpansionFraction = 0.5f
-
- whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
- whenever(ambientState.isShadeExpanded).thenReturn(true)
- whenever(ambientState.lastVisibleBackgroundChild).thenReturn(viewInShelf)
- whenever(ambientState.isExpansionChanging).thenReturn(true)
- whenever(ambientState.expansionFraction).thenReturn(shadeExpansionFraction)
- whenever(viewInShelf.viewState).thenReturn(ExpandableViewState())
- whenever(viewInShelf.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
- whenever(viewInShelf.translationY).thenReturn(shelfTop)
- whenever(viewInShelf.actualHeight).thenReturn(10)
- whenever(viewInShelf.isInShelf).thenReturn(true)
- whenever(viewInShelf.minHeight).thenReturn(10)
- whenever(viewInShelf.shelfTransformationTarget).thenReturn(null) // use translationY
- whenever(viewInShelf.isInShelf).thenReturn(true)
-
- stackScrollAlgorithmState.visibleChildren.add(viewInShelf)
- stackScrollAlgorithmState.firstViewInShelf = viewInShelf
-
- // WHEN Shelf's ViewState is updated
- shelf.updateState(stackScrollAlgorithmState, ambientState)
-
- // THEN the shelf is visible
- val shelfState = shelf.viewState as NotificationShelf.ShelfState
- assertEquals(false, shelfState.hidden)
- assertEquals(shelf.height, shelfState.height)
- // AND its translation is scaled by the shade expansion
- assertEquals((stackBottom * 0.75f) - shelf.height, shelfState.yTranslation)
- }
-
- @Test
- @EnableSceneContainer
fun updateState_withNullLastVisibleBackgroundChild_hideShelf_withSceneContainer() {
// GIVEN
- val stackCutoff = 200f
- val scrimPadding =
- context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
+ val stackTop = 200f
+ val stackHeight = 800f
+ whenever(ambientState.stackTop).thenReturn(stackTop)
+ whenever(ambientState.stackHeight).thenReturn(stackHeight)
val paddingBetweenElements =
context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
- whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
whenever(ambientState.isShadeExpanded).thenReturn(true)
val lastVisibleBackgroundChild = mock<ExpandableView>()
val expandableViewState = ExpandableViewState()
@@ -467,7 +424,7 @@
// THEN
val shelfState = shelf.viewState as NotificationShelf.ShelfState
assertEquals(true, shelfState.hidden)
- assertEquals(stackCutoff - scrimPadding + paddingBetweenElements, shelfState.yTranslation)
+ assertEquals(stackTop + stackHeight + paddingBetweenElements, shelfState.yTranslation)
}
@Test
@@ -501,12 +458,12 @@
@EnableSceneContainer
fun updateState_withNullFirstViewInShelf_hideShelf_withSceneContainer() {
// GIVEN
- val stackCutoff = 200f
- val scrimPadding =
- context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
+ val stackTop = 200f
+ val stackHeight = 800f
+ whenever(ambientState.stackTop).thenReturn(stackTop)
+ whenever(ambientState.stackHeight).thenReturn(stackHeight)
val paddingBetweenElements =
context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
- whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
whenever(ambientState.isShadeExpanded).thenReturn(true)
val lastVisibleBackgroundChild = mock<ExpandableView>()
val expandableViewState = ExpandableViewState()
@@ -522,7 +479,7 @@
// THEN
val shelfState = shelf.viewState as NotificationShelf.ShelfState
assertEquals(true, shelfState.hidden)
- assertEquals(stackCutoff - scrimPadding + paddingBetweenElements, shelfState.yTranslation)
+ assertEquals(stackTop + stackHeight + paddingBetweenElements, shelfState.yTranslation)
}
@Test
@@ -556,12 +513,12 @@
@EnableSceneContainer
fun updateState_withCollapsedShade_hideShelf_withSceneContainer() {
// GIVEN
- val stackCutoff = 200f
- val scrimPadding =
- context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
+ val stackTop = 200f
+ val stackHeight = 800f
+ whenever(ambientState.stackTop).thenReturn(stackTop)
+ whenever(ambientState.stackHeight).thenReturn(stackHeight)
val paddingBetweenElements =
context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
- whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
val lastVisibleBackgroundChild = mock<ExpandableView>()
val expandableViewState = ExpandableViewState()
whenever(lastVisibleBackgroundChild.viewState).thenReturn(expandableViewState)
@@ -577,7 +534,7 @@
// THEN
val shelfState = shelf.viewState as NotificationShelf.ShelfState
assertEquals(true, shelfState.hidden)
- assertEquals(stackCutoff - scrimPadding + paddingBetweenElements, shelfState.yTranslation)
+ assertEquals(stackTop + stackHeight + paddingBetweenElements, shelfState.yTranslation)
}
@Test
@@ -609,12 +566,12 @@
@Test
@EnableSceneContainer
- fun updateState_withHiddenSectionBeforeShelf_hideShelf_withSceneContianer() {
+ fun updateState_withHiddenSectionBeforeShelf_hideShelf_withSceneContainer() {
// GIVEN
- val stackCutoff = 200f
- whenever(ambientState.stackCutoff).thenReturn(stackCutoff)
- val scrimPadding =
- context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings)
+ val stackTop = 200f
+ val stackHeight = 800f
+ whenever(ambientState.stackTop).thenReturn(stackTop)
+ whenever(ambientState.stackHeight).thenReturn(stackHeight)
val paddingBetweenElements =
context.resources.getDimensionPixelSize(R.dimen.notification_divider_height)
whenever(ambientState.isShadeExpanded).thenReturn(true)
@@ -646,7 +603,7 @@
// THEN
val shelfState = shelf.viewState as NotificationShelf.ShelfState
assertEquals(true, shelfState.hidden)
- assertEquals(stackCutoff - scrimPadding + paddingBetweenElements, shelfState.yTranslation)
+ assertEquals(stackTop + stackHeight + paddingBetweenElements, shelfState.yTranslation)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 967f95e..770c424 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -262,15 +262,62 @@
}
@Test
- public void updateStackEndHeightAndStackHeight_normallyUpdatesBoth() {
- final float expansionFraction = 0.5f;
+ @EnableSceneContainer
+ public void updateStackEndHeightAndStackHeight_shadeFullyExpanded_withSceneContainer() {
+ final float stackTop = 200f;
+ final float stackCutoff = 1000f;
+ final float stackEndHeight = stackCutoff - stackTop;
+ mAmbientState.setStackTop(stackTop);
+ mAmbientState.setStackCutoff(stackCutoff);
mAmbientState.setStatusBarState(StatusBarState.KEYGUARD);
-
- // Validate that by default we update everything
clearInvocations(mAmbientState);
+
+ // WHEN shade is fully expanded
+ mStackScroller.updateStackEndHeightAndStackHeight(/* fraction = */ 1.0f);
+
+ // THEN stackHeight and stackEndHeight are the same
+ verify(mAmbientState).setStackEndHeight(stackEndHeight);
+ verify(mAmbientState).setStackHeight(stackEndHeight);
+ }
+
+ @Test
+ @EnableSceneContainer
+ public void updateStackEndHeightAndStackHeight_shadeExpanding_withSceneContainer() {
+ final float stackTop = 200f;
+ final float stackCutoff = 1000f;
+ final float stackEndHeight = stackCutoff - stackTop;
+ mAmbientState.setStackTop(stackTop);
+ mAmbientState.setStackCutoff(stackCutoff);
+ mAmbientState.setStatusBarState(StatusBarState.KEYGUARD);
+ clearInvocations(mAmbientState);
+
+ // WHEN shade is expanding
+ final float expansionFraction = 0.5f;
mStackScroller.updateStackEndHeightAndStackHeight(expansionFraction);
- verify(mAmbientState).setStackEndHeight(anyFloat());
- verify(mAmbientState).setStackHeight(anyFloat());
+
+ // THEN stackHeight is changed by the expansion frac
+ verify(mAmbientState).setStackEndHeight(stackEndHeight);
+ verify(mAmbientState).setStackHeight(stackEndHeight * 0.75f);
+ }
+
+ @Test
+ @EnableSceneContainer
+ public void updateStackEndHeightAndStackHeight_shadeOverscrolledToTop_withSceneContainer() {
+ // GIVEN stack scrolled over the top, stack top is negative
+ final float stackTop = -2000f;
+ final float stackCutoff = 1000f;
+ final float stackEndHeight = stackCutoff - stackTop;
+ mAmbientState.setStackTop(stackTop);
+ mAmbientState.setStackCutoff(stackCutoff);
+ mAmbientState.setStatusBarState(StatusBarState.KEYGUARD);
+ clearInvocations(mAmbientState);
+
+ // WHEN stack is updated
+ mStackScroller.updateStackEndHeightAndStackHeight(/* fraction = */ 1.0f);
+
+ // THEN stackHeight is measured from the stack top
+ verify(mAmbientState).setStackEndHeight(stackEndHeight);
+ verify(mAmbientState).setStackHeight(stackEndHeight);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 8401a19..b12c098 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -8,6 +8,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ShadeInterpolation.getContentAlpha
import com.android.systemui.dump.DumpManager
@@ -549,6 +550,7 @@
assertThat((footerView.viewState as FooterViewState).hideContent).isTrue()
}
+ @DisableFlags(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR)
@Test
fun resetViewStates_clearAllInProgress_allRowsRemoved_emptyShade_footerHidden() {
ambientState.isClearAllInProgress = true
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepositoryKosmos.kt
similarity index 88%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepositoryKosmos.kt
index d8af3fa..2f5daaa 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/FixedColumnsRepositoryKosmos.kt
@@ -18,4 +18,4 @@
import com.android.systemui.kosmos.Kosmos
-val Kosmos.infiniteGridSizeRepository by Kosmos.Fixture { InfiniteGridSizeRepository() }
+val Kosmos.fixedColumnsRepository by Kosmos.Fixture { FixedColumnsRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryKosmos.kt
similarity index 69%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryKosmos.kt
index d8af3fa..696c4bf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/InfiniteGridSizeRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/PaginatedGridRepositoryKosmos.kt
@@ -16,6 +16,12 @@
package com.android.systemui.qs.panels.data.repository
+import com.android.systemui.common.ui.data.repository.configurationRepository
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
-val Kosmos.infiniteGridSizeRepository by Kosmos.Fixture { InfiniteGridSizeRepository() }
+val Kosmos.paginatedGridRepository by
+ Kosmos.Fixture {
+ testCase.context.orCreateTestableResources
+ PaginatedGridRepository(testCase.context.resources, configurationRepository)
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractorKosmos.kt
similarity index 78%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractorKosmos.kt
index 6e11977..f4d281d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/FixedColumnsSizeInteractorKosmos.kt
@@ -17,7 +17,7 @@
package com.android.systemui.qs.panels.domain.interactor
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.data.repository.infiniteGridSizeRepository
+import com.android.systemui.qs.panels.data.repository.fixedColumnsRepository
-val Kosmos.infiniteGridSizeInteractor by
- Kosmos.Fixture { InfiniteGridSizeInteractor(infiniteGridSizeRepository) }
+val Kosmos.fixedColumnsSizeInteractor by
+ Kosmos.Fixture { FixedColumnsSizeInteractor(fixedColumnsRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt
index 7f387d7..320c2ec 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridConsistencyInteractorKosmos.kt
@@ -20,5 +20,5 @@
val Kosmos.infiniteGridConsistencyInteractor by
Kosmos.Fixture {
- InfiniteGridConsistencyInteractor(iconTilesInteractor, infiniteGridSizeInteractor)
+ InfiniteGridConsistencyInteractor(iconTilesInteractor, fixedColumnsSizeInteractor)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
index 82cfaf5..be00152 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridLayoutKosmos.kt
@@ -18,8 +18,8 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
+import com.android.systemui.qs.panels.ui.viewmodel.fixedColumnsSizeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.iconTilesViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.infiniteGridSizeViewModel
val Kosmos.infiniteGridLayout by
- Kosmos.Fixture { InfiniteGridLayout(iconTilesViewModel, infiniteGridSizeViewModel) }
+ Kosmos.Fixture { InfiniteGridLayout(iconTilesViewModel, fixedColumnsSizeViewModel) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractorKosmos.kt
similarity index 78%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractorKosmos.kt
index 6e11977..a922e5d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/InfiniteGridSizeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/PaginatedGridInteractorKosmos.kt
@@ -17,7 +17,7 @@
package com.android.systemui.qs.panels.domain.interactor
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.data.repository.infiniteGridSizeRepository
+import com.android.systemui.qs.panels.data.repository.paginatedGridRepository
-val Kosmos.infiniteGridSizeInteractor by
- Kosmos.Fixture { InfiniteGridSizeInteractor(infiniteGridSizeRepository) }
+val Kosmos.paginatedGridInteractor by
+ Kosmos.Fixture { PaginatedGridInteractor(paginatedGridRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModelKosmos.kt
similarity index 79%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModelKosmos.kt
index f6dfb8b..feadc91 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/FixedColumnsSizeViewModelKosmos.kt
@@ -17,7 +17,7 @@
package com.android.systemui.qs.panels.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.domain.interactor.infiniteGridSizeInteractor
+import com.android.systemui.qs.panels.domain.interactor.fixedColumnsSizeInteractor
-val Kosmos.infiniteGridSizeViewModel by
- Kosmos.Fixture { InfiniteGridSizeViewModelImpl(infiniteGridSizeInteractor) }
+val Kosmos.fixedColumnsSizeViewModel by
+ Kosmos.Fixture { FixedColumnsSizeViewModelImpl(fixedColumnsSizeInteractor) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MockTileViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MockTileViewModel.kt
new file mode 100644
index 0000000..5386ece
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MockTileViewModel.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+fun MockTileViewModel(
+ spec: TileSpec,
+ state: StateFlow<QSTile.State> = MutableStateFlow(QSTile.State())
+): TileViewModel = mock {
+ whenever(this.spec).thenReturn(spec)
+ whenever(this.state).thenReturn(state)
+ whenever(this.currentState).thenReturn(state.value)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
similarity index 62%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
index f6dfb8b..85e9265 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridSizeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt
@@ -17,7 +17,16 @@
package com.android.systemui.qs.panels.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.qs.panels.domain.interactor.infiniteGridSizeInteractor
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.qs.panels.domain.interactor.paginatedGridInteractor
-val Kosmos.infiniteGridSizeViewModel by
- Kosmos.Fixture { InfiniteGridSizeViewModelImpl(infiniteGridSizeInteractor) }
+val Kosmos.paginatedGridViewModel by
+ Kosmos.Fixture {
+ PaginatedGridViewModel(
+ iconTilesViewModel,
+ fixedColumnsSizeViewModel,
+ iconLabelVisibilityViewModel,
+ paginatedGridInteractor,
+ applicationCoroutineScope,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt
index b07cc7d..fde174d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PartitionedGridViewModelKosmos.kt
@@ -22,7 +22,7 @@
Kosmos.Fixture {
PartitionedGridViewModel(
iconTilesViewModel,
- infiniteGridSizeViewModel,
+ fixedColumnsSizeViewModel,
iconLabelVisibilityViewModel,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
index 1851c89..6574946 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
@@ -36,3 +36,14 @@
val Kosmos.mockSystemUIDialogFactory: SystemUIDialog.Factory by
Kosmos.Fixture { mock<SystemUIDialog.Factory>() }
+
+val Kosmos.systemUIDialogDotFactory by
+ Kosmos.Fixture {
+ SystemUIDialog.Factory(
+ applicationContext,
+ systemUIDialogManager,
+ sysUiState,
+ broadcastDispatcher,
+ dialogTransitionAnimator,
+ )
+ }
diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
index b64aa8a..ea6351b 100644
--- a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
+++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
@@ -364,7 +364,7 @@
}
@RequiresPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS)
- private int invokeContextualSearchIntent(Intent launchIntent) {
+ private int invokeContextualSearchIntent(Intent launchIntent, final int userId) {
// Contextual search starts with a frozen screen - so we launch without
// any system animations or starting window.
final ActivityOptions opts = ActivityOptions.makeCustomTaskAnimation(mContext,
@@ -372,7 +372,7 @@
opts.setDisableStartingWindow(true);
return mAtmInternal.startActivityWithScreenshot(launchIntent,
mContext.getPackageName(), Binder.getCallingUid(), Binder.getCallingPid(), null,
- opts.toBundle(), Binder.getCallingUserHandle().getIdentifier());
+ opts.toBundle(), userId);
}
private void enforcePermission(@NonNull final String func) {
@@ -446,6 +446,8 @@
synchronized (this) {
if (DEBUG_USER) Log.d(TAG, "startContextualSearch");
enforcePermission("startContextualSearch");
+ final int callingUserId = Binder.getCallingUserHandle().getIdentifier();
+
mAssistDataRequester.cancel();
// Creates a new CallbackToken at mToken and an expiration handler.
issueToken();
@@ -455,7 +457,7 @@
Binder.withCleanCallingIdentity(() -> {
Intent launchIntent = getContextualSearchIntent(entrypoint, mToken);
if (launchIntent != null) {
- int result = invokeContextualSearchIntent(launchIntent);
+ int result = invokeContextualSearchIntent(launchIntent, callingUserId);
if (DEBUG_USER) Log.d(TAG, "Launch result: " + result);
}
});
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index b35959f..19279a8 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -104,6 +104,7 @@
import android.os.ServiceSpecificException;
import android.os.SystemClock;
import android.os.SystemProperties;
+import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.DiskInfo;
@@ -1180,6 +1181,7 @@
private void onUserUnlocking(int userId) {
Slog.d(TAG, "onUserUnlocking " + userId);
+ Trace.instant(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.onUserUnlocking: " + userId);
if (userId != UserHandle.USER_SYSTEM) {
// Check if this user shares media with another user
@@ -1466,6 +1468,8 @@
@Override
public void onVolumeCreated(String volId, int type, String diskId, String partGuid,
int userId) {
+ Trace.instant(Trace.TRACE_TAG_SYSTEM_SERVER,
+ "SMS.onVolumeCreated: " + volId + ", " + userId);
synchronized (mLock) {
final DiskInfo disk = mDisks.get(diskId);
final VolumeInfo vol = new VolumeInfo(volId, type, disk, partGuid);
@@ -2352,6 +2356,7 @@
private void mount(VolumeInfo vol) {
try {
+ Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.mount: " + vol.id);
// TODO(b/135341433): Remove cautious logging when FUSE is stable
Slog.i(TAG, "Mounting volume " + vol);
extendWatchdogTimeout("#mount might be slow");
@@ -2363,6 +2368,8 @@
vol.internalPath = internalPath;
ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd);
try {
+ Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
+ "SMS.startFuseFileSystem: " + vol.id);
mStorageSessionController.onVolumeMount(pfd, vol);
return true;
} catch (ExternalStorageServiceException e) {
@@ -2375,6 +2382,7 @@
TimeUnit.SECONDS.toMillis(nextResetSeconds));
return false;
} finally {
+ Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
try {
pfd.close();
} catch (Exception e) {
@@ -2386,6 +2394,8 @@
Slog.i(TAG, "Mounted volume " + vol);
} catch (Exception e) {
Slog.wtf(TAG, e);
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}
}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index bce1830..27fda15 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -414,7 +414,8 @@
}
if (!mScoManagedByAudio) {
boolean isBtScoRequested = isBluetoothScoRequested();
- if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive())) {
+ if (isBtScoRequested && (!wasBtScoRequested || !isBluetoothScoActive()
+ || !mBtHelper.isBluetoothScoRequestedInternally())) {
if (!mBtHelper.startBluetoothSco(scoAudioMode, eventSource)) {
Log.w(TAG, "setCommunicationRouteForClient: failure to start BT SCO for uid: "
+ uid);
@@ -1148,13 +1149,14 @@
}
/*package*/ void setBluetoothScoOn(boolean on, String eventSource) {
- if (AudioService.DEBUG_COMM_RTE) {
- Log.v(TAG, "setBluetoothScoOn: " + on + " " + eventSource);
- }
synchronized (mBluetoothAudioStateLock) {
+ boolean isBtScoRequested = isBluetoothScoRequested();
+ Log.i(TAG, "setBluetoothScoOn: " + on + ", mBluetoothScoOn: "
+ + mBluetoothScoOn + ", isBtScoRequested: " + isBtScoRequested
+ + ", from: " + eventSource);
mBluetoothScoOn = on;
updateAudioHalBluetoothState();
- postUpdateCommunicationRouteClient(isBluetoothScoRequested(), eventSource);
+ postUpdateCommunicationRouteClient(isBtScoRequested, eventSource);
}
}
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 991f94b..8008717 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -378,7 +378,6 @@
/*package*/ synchronized void onReceiveBtEvent(Intent intent) {
final String action = intent.getAction();
- Log.i(TAG, "onReceiveBtEvent action: " + action + " mScoAudioState: " + mScoAudioState);
if (action.equals(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) {
BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE,
android.bluetooth.BluetoothDevice.class);
@@ -405,6 +404,7 @@
private void onScoAudioStateChanged(int state) {
boolean broadcast = false;
int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;
+ Log.i(TAG, "onScoAudioStateChanged state: " + state + " mScoAudioState: " + mScoAudioState);
if (mDeviceBroker.isScoManagedByAudio()) {
switch (state) {
case BluetoothHeadset.STATE_AUDIO_CONNECTED:
@@ -488,6 +488,11 @@
== BluetoothHeadset.STATE_AUDIO_CONNECTED;
}
+ /*package*/ synchronized boolean isBluetoothScoRequestedInternally() {
+ return mScoAudioState == SCO_STATE_ACTIVE_INTERNAL
+ || mScoAudioState == SCO_STATE_ACTIVATE_REQ;
+ }
+
// @GuardedBy("mDeviceBroker.mSetModeLock")
@GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
/*package*/ synchronized boolean startBluetoothSco(int scoAudioMode,
diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
index 62adb25..a6b07de 100644
--- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
+++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
@@ -19,7 +19,6 @@
import android.annotation.AnyThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.UserIdInt;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
@@ -34,24 +33,13 @@
@GuardedBy("ImfLock.class")
private final ArrayList<InputMethodSubtypeHandle> mSubtypeHandles = new ArrayList<>();
- @UserIdInt
- private final int mUserId;
-
- @AnyThread
- @UserIdInt
- int getUserId() {
- return mUserId;
- }
-
- HardwareKeyboardShortcutController(@NonNull InputMethodMap methodMap, @UserIdInt int userId) {
- mUserId = userId;
- reset(methodMap);
+ HardwareKeyboardShortcutController(@NonNull InputMethodSettings settings) {
+ update(settings);
}
@GuardedBy("ImfLock.class")
- void reset(@NonNull InputMethodMap methodMap) {
+ void update(@NonNull InputMethodSettings settings) {
mSubtypeHandles.clear();
- final InputMethodSettings settings = InputMethodSettings.create(methodMap, mUserId);
final List<InputMethodInfo> inputMethods = settings.getEnabledInputMethodList();
for (int i = 0; i < inputMethods.size(); ++i) {
final InputMethodInfo imi = inputMethods.get(i);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 39262c5..af2bc34 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -381,10 +381,6 @@
@NonNull
@MultiUserUnawareField
private InputMethodSubtypeSwitchingController mSwitchingController;
- // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
- @NonNull
- @MultiUserUnawareField
- private HardwareKeyboardShortcutController mHardwareKeyboardShortcutController;
@Nullable
private StatusBarManagerInternal mStatusBarManagerInternal;
@@ -1302,9 +1298,7 @@
mSwitchingController = new InputMethodSubtypeSwitchingController(context,
settings.getMethodMap(), settings.getUserId());
- mHardwareKeyboardShortcutController =
- new HardwareKeyboardShortcutController(settings.getMethodMap(),
- settings.getUserId());
+ getUserData(mCurrentUserId).mHardwareKeyboardShortcutController.update(settings);
mMenuController = new InputMethodMenuController(this);
mVisibilityStateComputer = new ImeVisibilityStateComputer(this);
mVisibilityApplier = new DefaultImeVisibilityApplier(this);
@@ -2936,7 +2930,6 @@
* </li>
* <li>{@link InputMethodBindingController#getDeviceIdToShowIme()} is ignored.</li>
* <li>{@link #mSwitchingController} is ignored.</li>
- * <li>{@link #mHardwareKeyboardShortcutController} is ignored.</li>
* <li>{@link #mPreventImeStartupUnlessTextEditor} is ignored.</li>
* <li>and so on.</li>
* </ul>
@@ -2969,6 +2962,9 @@
id = imi.getId();
settings.putSelectedInputMethod(id);
}
+
+ final var userData = getUserData(userId);
+ userData.mHardwareKeyboardShortcutController.update(settings);
}
@GuardedBy("ImfLock.class")
@@ -3051,13 +3047,7 @@
mSwitchingController = new InputMethodSubtypeSwitchingController(mContext,
settings.getMethodMap(), userId);
}
- // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
- if (userId == mHardwareKeyboardShortcutController.getUserId()) {
- mHardwareKeyboardShortcutController.reset(settings.getMethodMap());
- } else {
- mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
- settings.getMethodMap(), userId);
- }
+ getUserData(userId).mHardwareKeyboardShortcutController.update(settings);
sendOnNavButtonFlagsChangedLocked();
}
@@ -5322,13 +5312,7 @@
mSwitchingController = new InputMethodSubtypeSwitchingController(mContext,
settings.getMethodMap(), mCurrentUserId);
}
- // TODO: Instantiate mHardwareKeyboardShortcutController for each user.
- if (userId == mHardwareKeyboardShortcutController.getUserId()) {
- mHardwareKeyboardShortcutController.reset(settings.getMethodMap());
- } else {
- mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
- settings.getMethodMap(), userId);
- }
+ getUserData(userId).mHardwareKeyboardShortcutController.update(settings);
sendOnNavButtonFlagsChangedLocked();
@@ -5639,8 +5623,8 @@
final InputMethodSubtypeHandle currentSubtypeHandle =
InputMethodSubtypeHandle.of(currentImi, bindingController.getCurrentSubtype());
final InputMethodSubtypeHandle nextSubtypeHandle =
- mHardwareKeyboardShortcutController.onSubtypeSwitch(currentSubtypeHandle,
- direction > 0);
+ getUserData(userId).mHardwareKeyboardShortcutController.onSubtypeSwitch(
+ currentSubtypeHandle, direction > 0);
if (nextSubtypeHandle == null) {
return;
}
diff --git a/services/core/java/com/android/server/inputmethod/UserDataRepository.java b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
index 2b19d3e..5da4e89 100644
--- a/services/core/java/com/android/server/inputmethod/UserDataRepository.java
+++ b/services/core/java/com/android/server/inputmethod/UserDataRepository.java
@@ -88,6 +88,9 @@
@NonNull
final InputMethodBindingController mBindingController;
+ @NonNull
+ final HardwareKeyboardShortcutController mHardwareKeyboardShortcutController;
+
/**
* Intended to be instantiated only from this file.
*/
@@ -95,6 +98,8 @@
@NonNull InputMethodBindingController bindingController) {
mUserId = userId;
mBindingController = bindingController;
+ mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
+ InputMethodSettings.createEmptyMap(userId));
}
@Override
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index b89120b..b846947 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -650,9 +650,12 @@
synchronized (mLock) {
if (mLastWallpaper != null) {
WallpaperData targetWallpaper = null;
- if (mLastWallpaper.connection.containsDisplay(displayId)) {
+ if (mLastWallpaper.connection != null &&
+ mLastWallpaper.connection.containsDisplay(displayId)) {
targetWallpaper = mLastWallpaper;
- } else if (mFallbackWallpaper.connection.containsDisplay(displayId)) {
+ } else if (mFallbackWallpaper != null &&
+ mFallbackWallpaper.connection != null &&
+ mFallbackWallpaper.connection.containsDisplay(displayId)) {
targetWallpaper = mFallbackWallpaper;
}
if (targetWallpaper == null) return;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 9bc4389..159286e 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8293,7 +8293,8 @@
*/
@Override
protected int getOverrideOrientation() {
- return mLetterboxUiController.overrideOrientationIfNeeded(super.getOverrideOrientation());
+ return mAppCompatController.getOrientationPolicy()
+ .overrideOrientationIfNeeded(super.getOverrideOrientation());
}
/**
@@ -10825,7 +10826,8 @@
proto.write(SHOULD_OVERRIDE_MIN_ASPECT_RATIO,
mLetterboxUiController.shouldOverrideMinAspectRatio());
proto.write(SHOULD_IGNORE_ORIENTATION_REQUEST_LOOP,
- mLetterboxUiController.shouldIgnoreOrientationRequestLoop());
+ mAppCompatController.getAppCompatCapability().getAppCompatOrientationCapability()
+ .shouldIgnoreOrientationRequestLoop());
proto.write(SHOULD_OVERRIDE_FORCE_RESIZE_APP,
mLetterboxUiController.shouldOverrideForceResizeApp());
proto.write(SHOULD_ENABLE_USER_ASPECT_RATIO_SETTINGS,
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 17547f5..e0d2035 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -66,7 +66,6 @@
import android.annotation.Nullable;
import android.app.ActivityManager.TaskDescription;
import android.app.CameraCompatTaskInfo.FreeformCameraCompatMode;
-import android.content.pm.ActivityInfo.ScreenOrientation;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -151,27 +150,6 @@
}
}
- /**
- * Whether an app is calling {@link android.app.Activity#setRequestedOrientation}
- * in a loop and orientation request should be ignored.
- *
- * <p>This should only be called once in response to
- * {@link android.app.Activity#setRequestedOrientation}. See
- * {@link #shouldIgnoreRequestedOrientation} for more details.
- *
- * <p>This treatment is enabled when the following conditions are met:
- * <ul>
- * <li>Flag gating the treatment is enabled
- * <li>Opt-out component property isn't enabled
- * <li>Per-app override is enabled
- * <li>App has requested orientation more than 2 times within 1-second
- * timer and activity is not letterboxed for fixed orientation
- * </ul>
- */
- boolean shouldIgnoreOrientationRequestLoop() {
- return getAppCompatCapability().getAppCompatOrientationCapability()
- .shouldIgnoreOrientationRequestLoop();
- }
@VisibleForTesting
int getSetOrientationRequestCounter() {
@@ -299,12 +277,6 @@
return getAppCompatCapability().shouldUseDisplayLandscapeNaturalOrientation();
}
- @ScreenOrientation
- int overrideOrientationIfNeeded(@ScreenOrientation int candidate) {
- return mActivityRecord.mAppCompatController.getOrientationPolicy()
- .overrideOrientationIfNeeded(candidate);
- }
-
boolean isOverrideOrientationOnlyForCameraEnabled() {
return getAppCompatCapability().isOverrideOrientationOnlyForCameraEnabled();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
new file mode 100644
index 0000000..2260999
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatOrientationPolicyTest.java
@@ -0,0 +1,570 @@
+/*
+ * 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.server.wm;
+
+import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
+import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER;
+import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
+import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.NonNull;
+
+import com.android.server.wm.utils.TestComponentStack;
+
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+/**
+ * Test class for {@link AppCompatOrientationPolicy}.
+ * <p>
+ * Build/Install/Run:
+ * atest WmTests:AppCompatOrientationPolicyTest
+ */
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class AppCompatOrientationPolicyTest extends WindowTestsBase {
+
+ @Rule
+ public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
+ @Test
+ public void testOverrideOrientationIfNeeded_mapInvokedOnRequest() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.overrideOrientationIfNeeded(SCREEN_ORIENTATION_PORTRAIT);
+ robot.checkOrientationRequestMapped();
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+ public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUser() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.configureSetIgnoreOrientationRequest(true);
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+ public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_optOut_isUnchanged() {
+ runTestScenario((robot) -> {
+ robot.disableProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
+ robot.createActivityWithComponent();
+ robot.configureSetIgnoreOrientationRequest(true);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+ public void testOverrideOrientationIfNeeded_fullscreenOverrides_optOutSystem_returnsUser() {
+ runTestScenario((robot) -> {
+ robot.disableProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
+ robot.configureIsUserAppAspectRatioFullscreenEnabled(true);
+
+ robot.createActivityWithComponent();
+ robot.configureSetIgnoreOrientationRequest(true);
+ robot.prepareGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_FULLSCREEN);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+ public void testOverrideOrientationIfNeeded_fullscreenOverrides_optOutUser_returnsUser() {
+ runTestScenario((robot) -> {
+ robot.disableProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE);
+ robot.configureIsUserAppAspectRatioFullscreenEnabled(true);
+
+ robot.createActivityWithComponent();
+ robot.configureSetIgnoreOrientationRequest(true);
+ robot.prepareGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_FULLSCREEN);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+ public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUnchanged()
+ throws Exception {
+ runTestScenarioWithActivity((robot) -> {
+ robot.configureSetIgnoreOrientationRequest(false);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
+ public void testOverrideOrientationIfNeeded_fullscreenAndUserOverrideEnabled_isUnchanged() {
+ runTestScenario((robot) -> {
+ robot.prepareIsUserAppAspectRatioSettingsEnabled(true);
+
+ robot.createActivityWithComponent();
+ robot.configureSetIgnoreOrientationRequest(true);
+ robot.prepareGetUserMinAspectRatioOverrideCode(USER_MIN_ASPECT_RATIO_3_2);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+ public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsPortrait() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
+ public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsNosensor() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_NOSENSOR);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
+ public void testOverrideOrientationIfNeeded_nosensorOverride_orientationFixed_isUnchanged() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(
+ /* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
+ public void testOverrideOrientationIfNeeded_reverseLandscape_portraitOrUndefined_isUnchanged() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(
+ /* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ robot.checkOverrideOrientation(
+ /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
+ public void testOverrideOrientationIfNeeded_reverseLandscape_Landscape_getsReverseLandscape() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_LANDSCAPE,
+ /* expected */ SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+ public void testOverrideOrientationIfNeeded_portraitOverride_orientationFixed_IsUnchanged() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_NOSENSOR,
+ /* expected */ SCREEN_ORIENTATION_NOSENSOR);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
+ public void testOverrideOrientationIfNeeded_portraitAndIgnoreFixedOverrides_returnsPortrait() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_NOSENSOR,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR, OVERRIDE_ANY_ORIENTATION})
+ public void testOverrideOrientationIfNeeded_noSensorAndIgnoreFixedOverrides_returnsNosensor() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_NOSENSOR);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
+ public void testOverrideOrientationIfNeeded_propertyIsFalse_isUnchanged()
+ throws Exception {
+ runTestScenario((robot) -> {
+ robot.disableProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
+
+ robot.createActivityWithComponent();
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
+ OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
+ public void testOverrideOrientationIfNeeded_whenCameraNotActive_isUnchanged() {
+ runTestScenario((robot) -> {
+ robot.configureIsCameraCompatTreatmentEnabled(true);
+ robot.configureIsCameraCompatTreatmentEnabledAtBuildTime(true);
+
+ robot.createActivityWithComponentInNewTask();
+ robot.prepareIsTopActivityEligibleForOrientationOverride(false);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
+ OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
+ public void testOverrideOrientationIfNeeded_whenCameraActive_returnsPortrait() {
+ runTestScenario((robot) -> {
+ robot.configureIsCameraCompatTreatmentEnabled(true);
+ robot.configureIsCameraCompatTreatmentEnabledAtBuildTime(true);
+
+ robot.createActivityWithComponentInNewTask();
+ robot.prepareIsTopActivityEligibleForOrientationOverride(true);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ public void testOverrideOrientationIfNeeded_userFullscreenOverride_returnsUser() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserFullscreenOverride(true);
+ robot.configureSetIgnoreOrientationRequest(true);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ public void testOverrideOrientationIfNeeded_fullscreenOverride_cameraActivity_unchanged() {
+ runTestScenario((robot) -> {
+ robot.configureIsCameraCompatTreatmentEnabled(true);
+ robot.configureIsCameraCompatTreatmentEnabledAtBuildTime(true);
+
+ robot.createActivityWithComponentInNewTask();
+ robot.configureIsTopActivityCameraActive(false);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ public void testOverrideOrientationIfNeeded_respectOrientationRequestOverUserFullScreen() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserFullscreenOverride(true);
+ robot.configureSetIgnoreOrientationRequest(false);
+
+ robot.checkOverrideOrientationIsNot(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* notExpected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
+ public void testOverrideOrientationIfNeeded_userFullScreenOverrideOverSystem_returnsUser() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserFullscreenOverride(true);
+ robot.configureSetIgnoreOrientationRequest(true);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
+ public void testOverrideOrientationIfNeeded_respectOrientationReqOverUserFullScreenAndSystem() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserFullscreenOverride(true);
+ robot.configureSetIgnoreOrientationRequest(false);
+
+ robot.checkOverrideOrientationIsNot(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* notExpected */ SCREEN_ORIENTATION_USER);
+ });
+ }
+
+ @Test
+ public void testOverrideOrientationIfNeeded_userFullScreenOverrideDisabled_returnsUnchanged() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserFullscreenOverride(false);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_PORTRAIT,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ });
+ }
+
+ @Test
+ public void testOverrideOrientationIfNeeded_userAspectRatioApplied_unspecifiedOverridden() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserMinAspectRatioOverride(true);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_LOCKED,
+ /* expected */ SCREEN_ORIENTATION_PORTRAIT);
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_LANDSCAPE,
+ /* expected */ SCREEN_ORIENTATION_LANDSCAPE);
+ });
+ }
+
+ @Test
+ public void testOverrideOrientationIfNeeded_userAspectRatioNotApplied_isUnchanged() {
+ runTestScenarioWithActivity((robot) -> {
+ robot.prepareShouldApplyUserFullscreenOverride(false);
+
+ robot.checkOverrideOrientation(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED,
+ /* expected */ SCREEN_ORIENTATION_UNSPECIFIED);
+ });
+ }
+
+
+ /**
+ * Runs a test scenario with an existing activity providing a Robot.
+ */
+ void runTestScenarioWithActivity(@NonNull Consumer<OrientationPolicyRobotTest> consumer) {
+ runTestScenario(/* withActivity */ true, consumer);
+ }
+
+ /**
+ * Runs a test scenario without an existing activity providing a Robot.
+ */
+ void runTestScenario(@NonNull Consumer<OrientationPolicyRobotTest> consumer) {
+ runTestScenario(/* withActivity */ false, consumer);
+ }
+
+ /**
+ * Runs a test scenario providing a Robot.
+ */
+ void runTestScenario(boolean withActivity,
+ @NonNull Consumer<OrientationPolicyRobotTest> consumer) {
+ spyOn(mWm.mLetterboxConfiguration);
+ final OrientationPolicyRobotTest robot =
+ new OrientationPolicyRobotTest(mWm, mAtm, mSupervisor, withActivity);
+ consumer.accept(robot);
+ }
+
+ private static class OrientationPolicyRobotTest {
+
+ @NonNull
+ private final ActivityTaskManagerService mAtm;
+ @NonNull
+ private final WindowManagerService mWm;
+ @NonNull
+ private final LetterboxConfiguration mLetterboxConfiguration;
+ @NonNull
+ private final TestComponentStack<ActivityRecord> mActivityStack;
+ @NonNull
+ private final TestComponentStack<Task> mTaskStack;
+
+ @NonNull
+ private final ActivityTaskSupervisor mSupervisor;
+
+ OrientationPolicyRobotTest(@NonNull WindowManagerService wm,
+ @NonNull ActivityTaskManagerService atm,
+ @NonNull ActivityTaskSupervisor supervisor,
+ boolean withActivity) {
+ mAtm = atm;
+ mWm = wm;
+ spyOn(mWm);
+ mSupervisor = supervisor;
+ mActivityStack = new TestComponentStack<>();
+ mTaskStack = new TestComponentStack<>();
+ mLetterboxConfiguration = mWm.mLetterboxConfiguration;
+ if (withActivity) {
+ createActivityWithComponent();
+ }
+ }
+
+ void configureSetIgnoreOrientationRequest(boolean enabled) {
+ mActivityStack.top().mDisplayContent.setIgnoreOrientationRequest(enabled);
+ }
+
+ void configureIsUserAppAspectRatioFullscreenEnabled(boolean enabled) {
+ doReturn(enabled).when(mLetterboxConfiguration).isUserAppAspectRatioFullscreenEnabled();
+ }
+
+ void configureIsCameraCompatTreatmentEnabled(boolean enabled) {
+ doReturn(enabled).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
+ }
+
+ void configureIsCameraCompatTreatmentEnabledAtBuildTime(boolean enabled) {
+ doReturn(enabled).when(mLetterboxConfiguration)
+ .isCameraCompatTreatmentEnabledAtBuildTime();
+ }
+
+ void prepareGetUserMinAspectRatioOverrideCode(int orientation) {
+ spyOn(mActivityStack.top().mLetterboxUiController);
+ doReturn(orientation).when(mActivityStack.top()
+ .mLetterboxUiController).getUserMinAspectRatioOverrideCode();
+ }
+
+ void prepareShouldApplyUserFullscreenOverride(boolean enabled) {
+ spyOn(mActivityStack.top().mLetterboxUiController);
+ doReturn(enabled).when(mActivityStack.top()
+ .mLetterboxUiController).shouldApplyUserFullscreenOverride();
+ }
+
+ void prepareShouldApplyUserMinAspectRatioOverride(boolean enabled) {
+ spyOn(mActivityStack.top().mLetterboxUiController);
+ doReturn(enabled).when(mActivityStack.top()
+ .mLetterboxUiController).shouldApplyUserMinAspectRatioOverride();
+ }
+
+ void prepareIsUserAppAspectRatioSettingsEnabled(boolean enabled) {
+ doReturn(enabled).when(mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled();
+ }
+
+ void prepareIsTopActivityEligibleForOrientationOverride(boolean enabled) {
+ final DisplayRotationCompatPolicy displayPolicy =
+ mActivityStack.top().mDisplayContent.mDisplayRotationCompatPolicy;
+ spyOn(displayPolicy);
+ doReturn(enabled).when(displayPolicy)
+ .isActivityEligibleForOrientationOverride(eq(mActivityStack.top()));
+ }
+
+ void configureIsTopActivityCameraActive(boolean enabled) {
+ final DisplayRotationCompatPolicy displayPolicy =
+ mActivityStack.top().mDisplayContent.mDisplayRotationCompatPolicy;
+ spyOn(displayPolicy);
+ doReturn(enabled).when(displayPolicy)
+ .isCameraActive(eq(mActivityStack.top()), /* mustBeFullscreen= */ eq(true));
+ }
+
+ void disableProperty(@NonNull String propertyName) {
+ setPropertyValue(propertyName, /* enabled */ false);
+ }
+
+ int overrideOrientationIfNeeded(@ActivityInfo.ScreenOrientation int candidate) {
+ return mActivityStack.top().mAppCompatController.getOrientationPolicy()
+ .overrideOrientationIfNeeded(candidate);
+ }
+
+ void checkOrientationRequestMapped() {
+ verify(mWm).mapOrientationRequest(SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ void checkOverrideOrientation(@ActivityInfo.ScreenOrientation int candidate,
+ @ActivityInfo.ScreenOrientation int expected) {
+ Assert.assertEquals(expected, overrideOrientationIfNeeded(candidate));
+ }
+
+ void checkOverrideOrientationIsNot(@ActivityInfo.ScreenOrientation int candidate,
+ @ActivityInfo.ScreenOrientation int notExpected) {
+ Assert.assertNotEquals(notExpected, overrideOrientationIfNeeded(candidate));
+ }
+
+ private void createActivityWithComponent() {
+ if (mTaskStack.isEmpty()) {
+ final DisplayContent displayContent = new TestDisplayContent
+ .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
+ final Task task = new TaskBuilder(mSupervisor).setDisplay(displayContent).build();
+ mTaskStack.push(task);
+ }
+ final ActivityRecord activity = new ActivityBuilder(mAtm)
+ .setOnTop(true)
+ .setTask(mTaskStack.top())
+ // Set the component to be that of the test class in order
+ // to enable compat changes
+ .setComponent(ComponentName.createRelative(mAtm.mContext,
+ com.android.server.wm.LetterboxUiControllerTest.class.getName()))
+ .build();
+ mActivityStack.push(activity);
+ }
+
+ private void createActivityWithComponentInNewTask() {
+ final DisplayContent displayContent = new TestDisplayContent
+ .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();
+ final Task task = new TaskBuilder(mSupervisor).setDisplay(displayContent).build();
+ final ActivityRecord activity = new ActivityBuilder(mAtm)
+ .setOnTop(true)
+ .setTask(task)
+ // Set the component to be that of the test class in order
+ // to enable compat changes
+ .setComponent(ComponentName.createRelative(mAtm.mContext,
+ com.android.server.wm.LetterboxUiControllerTest.class.getName()))
+ .build();
+ mTaskStack.push(task);
+ mActivityStack.push(activity);
+ }
+
+ private void setPropertyValue(@NonNull String propertyName, boolean enabled) {
+ PackageManager.Property property = new PackageManager.Property(propertyName,
+ /* value */ enabled, /* packageName */ "",
+ /* className */ "");
+ PackageManager pm = mWm.mContext.getPackageManager();
+ spyOn(pm);
+ try {
+ doReturn(property).when(pm).getProperty(eq(propertyName), anyString());
+ } catch (PackageManager.NameNotFoundException e) {
+ fail(e.getLocalizedMessage());
+ }
+ }
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index b1200bcd..bdd45c6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -18,27 +18,14 @@
import static android.content.pm.ActivityInfo.FORCE_NON_RESIZE_APP;
import static android.content.pm.ActivityInfo.FORCE_RESIZE_APP;
-import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION;
-import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_FAKE_FOCUS;
-import static android.content.pm.ActivityInfo.OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA;
-import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA;
-import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR;
-import static android.content.pm.ActivityInfo.OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT;
import static android.content.pm.ActivityInfo.OVERRIDE_USE_DISPLAY_LANDSCAPE_NATURAL_ORIENTATION;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
@@ -49,7 +36,6 @@
import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_DISPLAY_ORIENTATION_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
-import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
@@ -535,322 +521,6 @@
return mainWindow;
}
- // overrideOrientationIfNeeded
-
- @Test
- public void testOverrideOrientationIfNeeded_mapInvokedOnRequest() throws Exception {
- mController = new LetterboxUiController(mWm, mActivity);
- spyOn(mWm);
-
- mController.overrideOrientationIfNeeded(SCREEN_ORIENTATION_PORTRAIT);
-
- verify(mWm).mapOrientationRequest(SCREEN_ORIENTATION_PORTRAIT);
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
- public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUser()
- throws Exception {
- mDisplayContent.setIgnoreOrientationRequest(true);
- assertEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
- public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_optOut_returnsUnchanged()
- throws Exception {
- mockThatProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE, /* value */ false);
-
- mActivity = setUpActivityWithComponent();
- mController = new LetterboxUiController(mWm, mActivity);
- mDisplayContent.setIgnoreOrientationRequest(true);
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
- public void testOverrideOrientationIfNeeded_fullscreenOverrides_optOutSystem_returnsUser()
- throws Exception {
- mockThatProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE, /* value */ false);
- prepareActivityThatShouldApplyUserFullscreenOverride();
-
- // fullscreen override still applied
- assertEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
- public void testOverrideOrientationIfNeeded_fullscreenOverrides_optOutUser_returnsUser()
- throws Exception {
- mockThatProperty(PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE,
- /* value */ false);
- prepareActivityThatShouldApplyUserFullscreenOverride();
-
- // fullscreen override still applied
- assertEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
- public void testOverrideOrientationIfNeeded_fullscreenOverrideEnabled_returnsUnchanged()
- throws Exception {
- mDisplayContent.setIgnoreOrientationRequest(false);
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_ANY_ORIENTATION_TO_USER})
- public void testOverrideOrientationIfNeeded_fullscreenAndUserOverrideEnabled_returnsUnchanged()
- throws Exception {
- doReturn(true).when(mLetterboxConfiguration).isUserAppAspectRatioSettingsEnabled();
- mActivity = setUpActivityWithComponent();
- spyOn(mActivity.mLetterboxUiController);
- doReturn(USER_MIN_ASPECT_RATIO_3_2).when(mActivity.mLetterboxUiController)
- .getUserMinAspectRatioOverrideCode();
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mActivity.mLetterboxUiController
- .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
- public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsPortrait()
- throws Exception {
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
- public void testOverrideOrientationIfNeeded_portraitOverrideEnabled_returnsNosensor() {
- assertEquals(SCREEN_ORIENTATION_NOSENSOR, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR})
- public void testOverrideOrientationIfNeeded_nosensorOverride_orientationFixed_returnsUnchanged() {
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
- public void testOverrideOrientationIfNeeded_reverseLandscapeOverride_orientationPortraitOrUndefined_returnsUnchanged() {
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_LANDSCAPE_ORIENTATION_TO_REVERSE_LANDSCAPE})
- public void testOverrideOrientationIfNeeded_reverseLandscapeOverride_orientationLandscape_returnsReverseLandscape() {
- assertEquals(SCREEN_ORIENTATION_REVERSE_LANDSCAPE, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_LANDSCAPE));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
- public void testOverrideOrientationIfNeeded_portraitOverride_orientationFixed_returnsUnchanged() {
- assertEquals(SCREEN_ORIENTATION_NOSENSOR, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_NOSENSOR));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
- public void testOverrideOrientationIfNeeded_portraitAndIgnoreFixedOverrides_returnsPortrait() {
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_NOSENSOR));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_NOSENSOR, OVERRIDE_ANY_ORIENTATION})
- public void testOverrideOrientationIfNeeded_noSensorAndIgnoreFixedOverrides_returnsNosensor() {
- assertEquals(SCREEN_ORIENTATION_NOSENSOR, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT})
- public void testOverrideOrientationIfNeeded_propertyIsFalse_returnsUnchanged()
- throws Exception {
- mockThatProperty(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE, /* value */ false);
-
- mActivity = setUpActivityWithComponent();
- mController = new LetterboxUiController(mWm, mActivity);
-
- assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
- OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
- public void testOverrideOrientationIfNeeded_whenCameraNotActive_returnsUnchanged() {
- doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
- doReturn(true).when(mLetterboxConfiguration)
- .isCameraCompatTreatmentEnabledAtBuildTime();
-
- // Recreate DisplayContent with DisplayRotationCompatPolicy
- mActivity = setUpActivityWithComponent();
- mController = new LetterboxUiController(mWm, mActivity);
-
- spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
- doReturn(false).when(mDisplayContent.mDisplayRotationCompatPolicy)
- .isActivityEligibleForOrientationOverride(eq(mActivity));
-
- assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT,
- OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA})
- public void testOverrideOrientationIfNeeded_whenCameraActive_returnsPortrait() {
- doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
- doReturn(true).when(mLetterboxConfiguration)
- .isCameraCompatTreatmentEnabledAtBuildTime();
-
- // Recreate DisplayContent with DisplayRotationCompatPolicy
- mActivity = setUpActivityWithComponent();
- mController = new LetterboxUiController(mWm, mActivity);
-
- spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
- doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
- .isActivityEligibleForOrientationOverride(eq(mActivity));
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_userFullscreenOverride_returnsUser() {
- spyOn(mActivity.mLetterboxUiController);
- doReturn(true).when(mActivity.mLetterboxUiController)
- .shouldApplyUserFullscreenOverride();
- mDisplayContent.setIgnoreOrientationRequest(true);
-
- assertEquals(SCREEN_ORIENTATION_USER, mActivity.mAppCompatController
- .getOrientationPolicy()
- .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_userFullscreenOverride_cameraActivity_noChange() {
- doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
- doReturn(true).when(mLetterboxConfiguration)
- .isCameraCompatTreatmentEnabledAtBuildTime();
-
- // Recreate DisplayContent with DisplayRotationCompatPolicy
- mActivity = setUpActivityWithComponent();
- mController = new LetterboxUiController(mWm, mActivity);
- spyOn(mController);
- doReturn(true).when(mController).shouldApplyUserFullscreenOverride();
-
- spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
- doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
- .isCameraActive(mActivity, /* mustBeFullscreen= */ true);
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_systemFullscreenOverride_cameraActivity_noChange() {
- doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled();
- doReturn(true).when(mLetterboxConfiguration)
- .isCameraCompatTreatmentEnabledAtBuildTime();
-
- // Recreate DisplayContent with DisplayRotationCompatPolicy
- mActivity = setUpActivityWithComponent();
- mController = new LetterboxUiController(mWm, mActivity);
- spyOn(mController);
- doReturn(true).when(mController).isSystemOverrideToFullscreenEnabled();
-
- spyOn(mDisplayContent.mDisplayRotationCompatPolicy);
- doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy)
- .isCameraActive(mActivity, /* mustBeFullscreen= */ true);
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_respectOrientationRequestOverUserFullScreen() {
- spyOn(mController);
- doReturn(true).when(mController).shouldApplyUserFullscreenOverride();
- mDisplayContent.setIgnoreOrientationRequest(false);
-
- assertNotEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
- public void testOverrideOrientationIfNeeded_userFullScreenOverrideOverSystem_returnsUser() {
- spyOn(mActivity.mLetterboxUiController);
- doReturn(true).when(mActivity.mLetterboxUiController)
- .shouldApplyUserFullscreenOverride();
- mDisplayContent.setIgnoreOrientationRequest(true);
-
- assertEquals(SCREEN_ORIENTATION_USER, mActivity.mAppCompatController
- .getOrientationPolicy()
- .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- @EnableCompatChanges({OVERRIDE_UNDEFINED_ORIENTATION_TO_PORTRAIT, OVERRIDE_ANY_ORIENTATION})
- public void testOverrideOrientationIfNeeded_respectOrientationReqOverUserFullScreenAndSystem() {
- spyOn(mController);
- doReturn(true).when(mController).shouldApplyUserFullscreenOverride();
- mDisplayContent.setIgnoreOrientationRequest(false);
-
- assertNotEquals(SCREEN_ORIENTATION_USER, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_userFullScreenOverrideDisabled_returnsUnchanged() {
- spyOn(mController);
- doReturn(false).when(mController).shouldApplyUserFullscreenOverride();
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_PORTRAIT));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_userAspectRatioApplied_unspecifiedOverridden() {
- spyOn(mActivity.mLetterboxUiController);
- doReturn(true).when(mActivity.mLetterboxUiController)
- .shouldApplyUserMinAspectRatioOverride();
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mActivity.mLetterboxUiController
- .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
-
- assertEquals(SCREEN_ORIENTATION_PORTRAIT, mActivity.mLetterboxUiController
- .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_LOCKED));
-
- // unchanged if orientation is specified
- assertEquals(SCREEN_ORIENTATION_LANDSCAPE, mActivity.mLetterboxUiController
- .overrideOrientationIfNeeded(/* candidate */ SCREEN_ORIENTATION_LANDSCAPE));
- }
-
- @Test
- public void testOverrideOrientationIfNeeded_userAspectRatioNotApplied_returnsUnchanged() {
- spyOn(mController);
- doReturn(false).when(mController).shouldApplyUserMinAspectRatioOverride();
-
- assertEquals(SCREEN_ORIENTATION_UNSPECIFIED, mController.overrideOrientationIfNeeded(
- /* candidate */ SCREEN_ORIENTATION_UNSPECIFIED));
- }
-
// shouldApplyUser...Override
@Test
public void testShouldApplyUserFullscreenOverride_trueProperty_returnsFalse() throws Exception {
@@ -1528,12 +1198,6 @@
mDisplayContent.setIgnoreOrientationRequest(true);
}
- private void prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch() {
- doReturn(true).when(mLetterboxConfiguration)
- .isPolicyForIgnoringRequestedOrientationEnabled();
- mController.setRelaunchingAfterRequestedOrientationChanged(true);
- }
-
private ActivityRecord setUpActivityWithComponent() {
mDisplayContent = new TestDisplayContent
.Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build();